├── bqplot_image_gl ├── tests │ ├── __init__.py │ └── test_imagegl.py ├── _version.py ├── linesgl.py ├── viewlistener.py ├── __init__.py ├── imagegl.py └── interacts.py ├── setup.cfg ├── js ├── lib │ ├── version.js │ ├── embed.js │ ├── index.js │ ├── utils.js │ ├── labplugin.js │ ├── examples │ │ └── lines │ │ │ ├── index.js │ │ │ ├── Line2.js │ │ │ ├── LineSegments2.js │ │ │ ├── LineGeometry.js │ │ │ ├── LineSegmentsGeometry.js │ │ │ └── LineMaterial.js │ ├── extension.js │ ├── BrushEllipseSelectorModel.js │ ├── serialize.js │ ├── ViewListener.js │ ├── linesgl.js │ ├── MouseInteraction.js │ ├── contour.js │ ├── values.js │ ├── BrushEllipseSelector.js │ └── imagegl.js ├── README.md ├── shaders │ ├── image-vertex.glsl │ ├── scales-transform.glsl │ ├── scales-extra.glsl │ └── image-fragment.glsl ├── package.json └── webpack.config.js ├── MANIFEST.in ├── etc └── jupyter │ └── nbconfig │ └── notebook.d │ └── bqplot-image-gl.json ├── .gitignore ├── pyproject.toml ├── .check_extension.py ├── examples ├── .validate-notebooks.py ├── basic.ipynb ├── brush-ellipse.ipynb ├── lines.ipynb ├── mouse.ipynb ├── viewlistener.ipynb ├── contour.ipynb └── data.json ├── RELEASE.md ├── azure-pipelines.yml ├── tox.ini ├── LICENSE ├── README.md └── setup.py /bqplot_image_gl/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | -------------------------------------------------------------------------------- /js/lib/version.js: -------------------------------------------------------------------------------- 1 | export const version = require('../package.json').version; 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include bqplot_image_gl/static *.* 2 | recursive-include etc *.json 3 | recursive-include share *.* 4 | -------------------------------------------------------------------------------- /etc/jupyter/nbconfig/notebook.d/bqplot-image-gl.json: -------------------------------------------------------------------------------- 1 | { 2 | "load_extensions": { 3 | "bqplot-image-gl/extension": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | bqplot_image_gl/static 3 | dist 4 | node_modules 5 | .tox 6 | .ipynb_checkpoints 7 | .tmp 8 | __pycache__ 9 | share 10 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["jupyter-packaging~=0.7.12", 3 | "jupyterlab~=3.0.0", 4 | "setuptools>=40.8.0", 5 | "wheel"] 6 | build-backend = "setuptools.build_meta" 7 | -------------------------------------------------------------------------------- /js/README.md: -------------------------------------------------------------------------------- 1 | An ipywidget image widget for astronomical purposes 2 | 3 | Package Install 4 | --------------- 5 | 6 | **Prerequisites** 7 | - [node](http://nodejs.org/) 8 | 9 | ```bash 10 | npm install --save bqplot-image-gl 11 | ``` 12 | -------------------------------------------------------------------------------- /.check_extension.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from notebook.nbextensions import validate_nbextension 4 | 5 | if validate_nbextension('bqplot-image-gl/extension') != []: 6 | print("Issue detected with nbextension for bqplot-image-gl") 7 | sys.exit(1) 8 | -------------------------------------------------------------------------------- /js/shaders/image-vertex.glsl: -------------------------------------------------------------------------------- 1 | varying vec2 pixel_coordinate; 2 | 3 | void main(void) { 4 | vec4 view_position = modelViewMatrix * vec4(position,1.0); 5 | pixel_coordinate = view_position.xy; 6 | gl_Position = projectionMatrix * view_position; 7 | } 8 | -------------------------------------------------------------------------------- /js/lib/embed.js: -------------------------------------------------------------------------------- 1 | /*jshint esversion: 6 */ 2 | 3 | // Entry point for the unpkg bundle containing custom model definitions. 4 | // 5 | // It differs from the notebook bundle in that it does not need to define a 6 | // dynamic baseURL for the static assets and may load some css that would 7 | // already be loaded by the notebook otherwise. 8 | export * from './index' 9 | -------------------------------------------------------------------------------- /bqplot_image_gl/tests/test_imagegl.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import bqplot 3 | from bqplot_image_gl import ImageGL 4 | 5 | 6 | def test_astro_image(): 7 | data = np.zeros((2, 3)) 8 | color_scale = bqplot.ColorScale() 9 | scale_x = bqplot.LinearScale() 10 | scale_y = bqplot.LinearScale() 11 | ImageGL(image=data, scales={'image': color_scale, 'x': scale_x, 'y': scale_y}) 12 | -------------------------------------------------------------------------------- /js/lib/index.js: -------------------------------------------------------------------------------- 1 | /*jshint esversion: 6 */ 2 | 3 | // Export widget models and views, and the npm package version number. 4 | export * from "./imagegl.js"; 5 | export * from "./linesgl.js"; 6 | export * from "./contour.js"; 7 | export * from './BrushEllipseSelectorModel'; 8 | export * from './BrushEllipseSelector'; 9 | export * from './MouseInteraction' 10 | export * from './ViewListener' 11 | export * from './version.js' 12 | -------------------------------------------------------------------------------- /bqplot_image_gl/_version.py: -------------------------------------------------------------------------------- 1 | version_info = (1, 4, 2, 'final', 0) 2 | 3 | _specifier_ = {'alpha': 'a', 'beta': 'b', 'candidate': 'rc', 'final': ''} 4 | 5 | __version__ = '%s.%s.%s%s' % (version_info[0], 6 | version_info[1], 7 | version_info[2], 8 | '' if version_info[3] == 'final' 9 | else _specifier_[version_info[3]] + str(version_info[4])) 10 | -------------------------------------------------------------------------------- /js/lib/utils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const isTypedArray = require("is-typedarray"); 3 | 4 | 5 | export 6 | function applyStyles(d3el, styles) { 7 | Object.keys(styles).forEach(key => d3el.style(key, styles[key])); 8 | return d3el 9 | } 10 | 11 | export 12 | function applyAttrs(d3el, styles) { 13 | Object.keys(styles).forEach(key => d3el.attr(key, styles[key])); 14 | return d3el 15 | } 16 | 17 | export 18 | function is_typedarray(obj) { 19 | return isTypedArray(obj); 20 | } 21 | -------------------------------------------------------------------------------- /examples/.validate-notebooks.py: -------------------------------------------------------------------------------- 1 | import os 2 | import glob 3 | 4 | import nbformat 5 | from nbconvert.preprocessors import ExecutePreprocessor 6 | 7 | for notebook in glob.glob('**/*.ipynb', recursive=True): 8 | 9 | print("Running {0}".format(notebook)) 10 | 11 | with open(notebook) as f: 12 | nb = nbformat.read(f, as_version=4) 13 | 14 | ep = ExecutePreprocessor(timeout=600, kernel_name='python3') 15 | ep.preprocess(nb, {'metadata': {'path': os.path.dirname(notebook)}}) 16 | -------------------------------------------------------------------------------- /js/lib/labplugin.js: -------------------------------------------------------------------------------- 1 | /*jshint esversion: 6 */ 2 | 3 | var bqplot_gl_image = require('./index'); 4 | var base = require('@jupyter-widgets/base'); 5 | 6 | module.exports = { 7 | id: 'bqplot-image-gl', 8 | requires: [base.IJupyterWidgetRegistry], 9 | activate: function(app, widgets) { 10 | widgets.registerWidget({ 11 | name: 'bqplot-image-gl', 12 | version: bqplot_gl_image.version, 13 | exports: bqplot_gl_image 14 | }); 15 | }, 16 | autoStart: true 17 | }; 18 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | - To release a new version of bqplot-image-gl on PyPI: 2 | 3 | Update _version.py (set release version, remove 'dev') 4 | git add the _version.py file 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 bqplot-image-gl on NPM: 14 | 15 | ``` 16 | # clean out the `dist` and `node_modules` directories 17 | git clean -fdx 18 | npm install 19 | npm publish 20 | ``` 21 | -------------------------------------------------------------------------------- /js/shaders/scales-transform.glsl: -------------------------------------------------------------------------------- 1 | // added by bqplot-image-gl 2 | vec3 instanceStart_transformed = instanceStart; 3 | vec3 instanceEnd_transformed = instanceEnd; 4 | instanceStart_transformed.x = SCALE_X(instanceStart_transformed.x); 5 | instanceStart_transformed.y = SCALE_Y(instanceStart_transformed.y); 6 | instanceEnd_transformed.x = SCALE_X(instanceEnd_transformed.x); 7 | instanceEnd_transformed.y = SCALE_Y(instanceEnd_transformed.y); 8 | vec4 start = modelViewMatrix * vec4( instanceStart_transformed, 1.0 ); 9 | vec4 end = modelViewMatrix * vec4( instanceEnd_transformed, 1.0 ); 10 | 11 | 12 | // added by bqplot-image-gl 13 | -------------------------------------------------------------------------------- /bqplot_image_gl/linesgl.py: -------------------------------------------------------------------------------- 1 | import ipywidgets as widgets 2 | import bqplot 3 | from traitlets import Unicode 4 | from bqplot_image_gl._version import __version__ 5 | 6 | __all__ = ['LinesGL'] 7 | 8 | 9 | @widgets.register 10 | class LinesGL(bqplot.Lines): 11 | """An example widget.""" 12 | _view_name = Unicode('LinesGLView').tag(sync=True) 13 | _model_name = Unicode('LinesGLModel').tag(sync=True) 14 | _view_module = Unicode('bqplot-image-gl').tag(sync=True) 15 | _model_module = Unicode('bqplot-image-gl').tag(sync=True) 16 | _view_module_version = Unicode('^' + __version__).tag(sync=True) 17 | _model_module_version = Unicode('^' + __version__).tag(sync=True) 18 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | resources: 2 | repositories: 3 | - repository: OpenAstronomy 4 | type: github 5 | endpoint: glue-viz 6 | name: OpenAstronomy/azure-pipelines-templates 7 | ref: master 8 | 9 | trigger: 10 | branches: 11 | include: 12 | - '*' 13 | tags: 14 | include: 15 | - 'v*' 16 | 17 | jobs: 18 | 19 | - template: run-tox-env.yml@OpenAstronomy 20 | parameters: 21 | 22 | coverage: codecov 23 | 24 | envs: 25 | 26 | - linux: codestyle 27 | coverage: 'false' 28 | 29 | - linux: py38-test 30 | - macos: py37-test 31 | - windows: py36-test 32 | 33 | - linux: py38-notebooks 34 | - macos: py37-notebooks 35 | - windows: py36-notebooks 36 | -------------------------------------------------------------------------------- /js/lib/examples/lines/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // This file auto-generated with generate-wrappers.js 3 | // 4 | // Load all three.js python wrappers 5 | var loadedModules = [ 6 | require('./Line2.js'), 7 | require('./LineGeometry.js'), 8 | require('./LineMaterial.js'), 9 | require('./LineSegments2.js'), 10 | require('./LineSegmentsGeometry.js'), 11 | ]; 12 | 13 | for (var i in loadedModules) { 14 | if (loadedModules.hasOwnProperty(i)) { 15 | var loadedModule = loadedModules[i]; 16 | for (var target_name in loadedModule) { 17 | if (loadedModule.hasOwnProperty(target_name)) { 18 | module.exports[target_name] = loadedModule[target_name]; 19 | } 20 | } 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /js/shaders/scales-extra.glsl: -------------------------------------------------------------------------------- 1 | // we already have this in scales.glsl in ipyvolume, but not in bqplot 2 | #define SCALE_TYPE_LINEAR 1 3 | #define SCALE_TYPE_LOG 2 4 | 5 | #ifdef USE_SCALE_X 6 | uniform vec2 domain_x; 7 | uniform vec2 range_x; 8 | #if SCALE_TYPE_x == SCALE_TYPE_LINEAR 9 | #define SCALE_X(x) scale_transform_linear(x, range_x, domain_x) 10 | #elif SCALE_TYPE_x == SCALE_TYPE_LOG 11 | #define SCALE_X(x) scale_transform_log(x, range_x, domain_x) 12 | #endif 13 | #endif 14 | 15 | #ifdef USE_SCALE_Y 16 | uniform vec2 domain_y; 17 | uniform vec2 range_y; 18 | #if SCALE_TYPE_y == SCALE_TYPE_LINEAR 19 | #define SCALE_Y(x) scale_transform_linear(x, range_y, domain_y) 20 | #elif SCALE_TYPE_y == SCALE_TYPE_LOG 21 | #define SCALE_Y(x) scale_transform_log(x, range_y, domain_y) 22 | #endif 23 | #endif 24 | -------------------------------------------------------------------------------- /bqplot_image_gl/viewlistener.py: -------------------------------------------------------------------------------- 1 | import ipywidgets as widgets 2 | from ipywidgets.widgets import widget_serialization 3 | from traitlets import Unicode, Dict, Instance 4 | from bqplot_image_gl._version import __version__ 5 | 6 | __all__ = ['ViewListener'] 7 | 8 | 9 | @widgets.register 10 | class ViewListener(widgets.DOMWidget): 11 | _view_name = Unicode('ViewListener').tag(sync=True) 12 | _model_name = Unicode('ViewListenerModel').tag(sync=True) 13 | _view_module = Unicode('bqplot-image-gl').tag(sync=True) 14 | _model_module = Unicode('bqplot-image-gl').tag(sync=True) 15 | _view_module_version = Unicode('^' + __version__).tag(sync=True) 16 | _model_module_version = Unicode('^' + __version__).tag(sync=True) 17 | 18 | widget = Instance(widgets.Widget).tag(sync=True, **widget_serialization) 19 | css_selector = Unicode(None, allow_none=True).tag(sync=True) 20 | view_data = Dict().tag(sync=True) 21 | -------------------------------------------------------------------------------- /bqplot_image_gl/__init__.py: -------------------------------------------------------------------------------- 1 | from ._version import version_info, __version__ # noqa 2 | 3 | from .imagegl import * # noqa 4 | from .linesgl import * # noqa 5 | 6 | 7 | def _prefix(): 8 | import sys 9 | from pathlib import Path 10 | prefix = sys.prefix 11 | here = Path(__file__).parent 12 | # for when in dev mode 13 | if (here.parent / 'share/jupyter/nbextensions/bqplot-image-gl').exists(): 14 | prefix = here.parent 15 | return prefix 16 | 17 | 18 | def _jupyter_labextension_paths(): 19 | return [{ 20 | 'src': f'{_prefix()}/share/jupyter/labextensions/bqplot-image-gl/', 21 | 'dest': 'bqplot-image-gl', 22 | }] 23 | 24 | 25 | def _jupyter_nbextension_paths(): 26 | return [{ 27 | 'section': 'notebook', 28 | 'src': f'{_prefix()}/share/jupyter/nbextensions/bqplot-image-gl/', 29 | 'dest': 'bqplot-image-gl', 30 | 'require': 'bqplot-image-gl/extension' 31 | }] 32 | -------------------------------------------------------------------------------- /js/lib/extension.js: -------------------------------------------------------------------------------- 1 | /*jshint esversion: 6 */ 2 | 3 | // This file contains the javascript that is run when the notebook is loaded. 4 | // It contains some requirejs configuration and the `load_ipython_extension` 5 | // which is required for any notebook extension. 6 | // 7 | // Some static assets may be required by the custom widget javascript. The base 8 | // url for the notebook is not known at build time and is therefore computed 9 | // dynamically. 10 | __webpack_public_path__ = document.querySelector('body').getAttribute('data-base-url') + 'nbextensions/bqplot-image-gl'; 11 | 12 | 13 | // Configure requirejs 14 | if (window.require) { 15 | window.require.config({ 16 | map: { 17 | "*" : { 18 | "bqplot-image-gl": "nbextensions/bqplot-image-gl/index", 19 | } 20 | } 21 | }); 22 | } 23 | 24 | // Export the required load_ipython_extension 25 | module.exports = { 26 | load_ipython_extension: function() {} 27 | }; 28 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{36,37,38}-{test,notebooks} 3 | requires = pip >= 18.0 4 | setuptools >= 30.3.0 5 | isolated_build = true 6 | 7 | [testenv] 8 | changedir = 9 | test: .tmp/{envname} 10 | notebooks: examples 11 | deps = 12 | test: pytest 13 | test: pytest-cov 14 | notebooks: numpy 15 | notebooks: ipyvuetify 16 | notebooks: scikit-image 17 | # NOTE: the following is a temporary fix for the issue described in 18 | # https://github.com/voila-dashboards/voila/issues/728 19 | # and should be removed once the issue is fixed in jupyter-server 20 | test: pytest-tornasync 21 | extras = 22 | test: test 23 | notebooks: test 24 | commands = 25 | test: pip freeze 26 | test: pytest --pyargs bqplot_image_gl --cov bqplot_image_gl -p no:warnings {posargs} 27 | test: python {toxinidir}/.check_extension.py 28 | notebooks: python .validate-notebooks.py 29 | 30 | [testenv:codestyle] 31 | deps = flake8 32 | skip_install = true 33 | commands = 34 | flake8 --max-line-length=100 bqplot_image_gl 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Maarten Breddels 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /js/lib/examples/lines/Line2.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author WestLangley / http://github.com/WestLangley 3 | * 4 | */ 5 | 6 | var THREE = require('three'); 7 | var LineGeometry = require('./LineGeometry').LineGeometry; 8 | var LineMaterial = require('./LineMaterial').LineMaterial; 9 | 10 | 11 | var Line2 = function ( geometry, material ) { 12 | 13 | THREE.Mesh.call( this ); 14 | 15 | this.type = 'Line2'; 16 | 17 | this.geometry = geometry !== undefined ? geometry : new LineGeometry(); 18 | this.material = material !== undefined ? material : new LineMaterial( { color: Math.random() * 0xffffff } ); 19 | 20 | }; 21 | 22 | Line2.prototype = Object.assign( Object.create( THREE.Mesh.prototype ), { 23 | 24 | constructor: Line2, 25 | 26 | isLine2: true, 27 | 28 | // onBeforeRender: function( renderer, scene, camera, geometry, material, group ) { 29 | 30 | // if ( material.isLineMaterial ) { 31 | 32 | // var size = renderer.getSize(); 33 | 34 | // material.resolution = new THREE.Vector2(size.width, size.height); 35 | 36 | // } 37 | 38 | // }, 39 | 40 | copy: function ( source ) { 41 | 42 | // todo 43 | 44 | return this; 45 | 46 | } 47 | 48 | } ); 49 | 50 | module.exports = { 51 | Line2: Line2 52 | }; 53 | -------------------------------------------------------------------------------- /js/lib/examples/lines/LineSegments2.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author WestLangley / http://github.com/WestLangley 3 | * 4 | */ 5 | 6 | var THREE = require('three'); 7 | var LineSegmentsGeometry = require('./LineSegmentsGeometry').LineSegmentsGeometry; 8 | var LineMaterial = require('./LineMaterial').LineMaterial; 9 | 10 | 11 | var LineSegments2 = function ( geometry, material ) { 12 | 13 | THREE.Mesh.call( this ); 14 | 15 | this.type = 'LineSegments2'; 16 | 17 | this.geometry = geometry !== undefined ? geometry : new LineSegmentsGeometry(); 18 | this.material = material !== undefined ? material : new LineMaterial( { color: Math.random() * 0xffffff } ); 19 | 20 | }; 21 | 22 | LineSegments2.prototype = Object.assign( Object.create( THREE.Mesh.prototype ), { 23 | 24 | constructor: LineSegments2, 25 | 26 | isLineSegments2: true, 27 | 28 | onBeforeRender: function( renderer, scene, camera, geometry, material, group ) { 29 | 30 | if ( material.isLineMaterial ) { 31 | 32 | var size = renderer.getSize(); 33 | 34 | material.resolution = new THREE.Vector2(size.width, size.height); 35 | 36 | } 37 | 38 | }, 39 | 40 | copy: function ( source ) { 41 | 42 | // todo 43 | 44 | return this; 45 | 46 | }, 47 | 48 | } ); 49 | 50 | module.exports = { 51 | LineSegments2: LineSegments2 52 | }; 53 | -------------------------------------------------------------------------------- /js/lib/BrushEllipseSelectorModel.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var version = require('./version').version; 4 | 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | const BrushSelectorModel = require("bqplot").BrushSelectorModel; 7 | class BrushEllipseSelectorModel extends BrushSelectorModel { 8 | defaults() { 9 | return Object.assign({}, BrushSelectorModel.prototype.defaults(), { 10 | _model_module: 'bqplot-image-gl', 11 | _view_module: 'bqplot-image-gl', 12 | _model_module_version: version, 13 | _view_module_version: version, 14 | _model_name: "BrushEllipseSelectorModel", 15 | _view_name: "BrushEllipseSelector", 16 | pixel_aspect: null, 17 | style: { 18 | fill: "green", 19 | opacity: 0.3, 20 | cursor: "grab", 21 | }, border_style: { 22 | fill: "none", 23 | stroke: "green", 24 | opacity: 0.3, 25 | cursor: "col-resize", 26 | "stroke-width": "3px", 27 | } 28 | }); 29 | } 30 | } 31 | BrushEllipseSelectorModel.serializers = Object.assign({}, BrushSelectorModel.serializers); 32 | exports.BrushEllipseSelectorModel = BrushEllipseSelectorModel; 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bqplot-image-gl 2 | 3 | An ipywidget image widget for showing images in bqplot using WebGL. 4 | Used for https://github.com/glue-viz/glue-jupyter 5 | 6 | (currently requires latest developer version of bqplot) 7 | 8 | # Installation 9 | 10 | To install use pip: 11 | 12 | $ pip install bqplot-image-gl 13 | 14 | # Installation (developers) 15 | 16 | # make sure you have node 17 | $ conda install -c conda-forge nodejs 18 | 19 | # clone the repo 20 | $ git clone https://github.com/glue-viz/bqplot-image-gl.git 21 | $ cd bqplot-image-gl 22 | 23 | # install in dev mode 24 | $ pip install -e . 25 | # symlink the share/jupyter/nbextensions/bqplot-image-gl directory 26 | $ jupyter nbextension install --py --symlink --sys-prefix --overwrite bqplot_image_gl 27 | # enable the extension (normally done by copying the .json in your prefix) 28 | $ jupyter nbextension enable --py --sys-prefix bqplot_image_gl 29 | # for jupyterlab (>=3.0), symlink share/jupyter/labextensions/bqplot-image-gl 30 | $ jupyter labextension develop . --overwrite 31 | 32 | ## workflow for notebook 33 | 34 | $ (cd js; npm run watch:nbextension) 35 | # make changes and wait for bundle to automatically rebuild 36 | # reload jupyter notebook 37 | 38 | ## workflow for lab 39 | 40 | $ (cd js; npm run watch:labextension) 41 | # make changes and wait for bundle to automatically rebuild 42 | # reload jupyterlab 43 | -------------------------------------------------------------------------------- /js/shaders/image-fragment.glsl: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | varying vec2 pixel_coordinate; 4 | uniform sampler2D image; 5 | uniform sampler2D colormap; 6 | uniform float color_min; 7 | uniform float color_max; 8 | uniform float opacity; 9 | 10 | uniform vec2 range_x; 11 | uniform vec2 range_y; 12 | 13 | uniform vec2 domain_x; 14 | uniform vec2 domain_y; 15 | 16 | uniform vec2 image_domain_x; 17 | uniform vec2 image_domain_y; 18 | 19 | bool isnan(float val) 20 | { 21 | return (val < 0.0 || 0.0 < val || val == 0.0) ? false : true; 22 | } 23 | 24 | 25 | 26 | void main(void) { 27 | // bring pixels(range) to world space (domain) 28 | float x_domain_value = scale_transform_linear_inverse(pixel_coordinate.x, range_x, domain_x); 29 | float y_domain_value = scale_transform_linear_inverse(pixel_coordinate.y, range_y, domain_y); 30 | // normalize the coordinates for the texture 31 | float x_normalized = scale_transform_linear(x_domain_value, vec2(0., 1.), image_domain_x); 32 | float y_normalized = scale_transform_linear(y_domain_value, vec2(0., 1.), image_domain_y); 33 | vec2 tex_uv = vec2(x_normalized, y_normalized); 34 | #ifdef USE_COLORMAP 35 | float raw_value = texture2D(image, tex_uv).r; 36 | float value = (raw_value - color_min) / (color_max - color_min); 37 | vec4 color; 38 | if(isnan(value)) // nan's are interpreted as missing values, and 'not shown' 39 | color = vec4(0., 0., 0., 0.); 40 | else 41 | color = texture2D(colormap, vec2(value, 0.5)); 42 | #else 43 | vec4 color = texture2D(image, tex_uv); 44 | #endif 45 | // since we're working with pre multiplied colors (regarding blending) 46 | // we also need to multiply rgb by opacity 47 | gl_FragColor = color * opacity; 48 | } 49 | -------------------------------------------------------------------------------- /examples/basic.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import numpy as np\n", 10 | "from bqplot import Figure, LinearScale, Axis, ColorScale\n", 11 | "from bqplot_image_gl import ImageGL" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "scale_x = LinearScale(min=0, max=1)\n", 21 | "scale_y = LinearScale(min=0, max=1)\n", 22 | "scales = {'x': scale_x,\n", 23 | " 'y': scale_y}\n", 24 | "axis_x = Axis(scale=scale_x, label='x')\n", 25 | "axis_y = Axis(scale=scale_y, label='y', orientation='vertical')\n", 26 | "\n", 27 | "figure = Figure(scales=scales, axes=[axis_x, axis_y])\n", 28 | "\n", 29 | "scales_image = {'x': scale_x,\n", 30 | " 'y': scale_y,\n", 31 | " 'image': ColorScale(min=0, max=1)}\n", 32 | "\n", 33 | "image = ImageGL(image=np.random.random((10, 10)), scales=scales_image)\n", 34 | "\n", 35 | "figure.marks = (image,)" 36 | ] 37 | }, 38 | { 39 | "cell_type": "code", 40 | "execution_count": null, 41 | "metadata": {}, 42 | "outputs": [], 43 | "source": [ 44 | "figure" 45 | ] 46 | } 47 | ], 48 | "metadata": { 49 | "kernelspec": { 50 | "display_name": "Python 3", 51 | "language": "python", 52 | "name": "python3" 53 | }, 54 | "language_info": { 55 | "codemirror_mode": { 56 | "name": "ipython", 57 | "version": 3 58 | }, 59 | "file_extension": ".py", 60 | "mimetype": "text/x-python", 61 | "name": "python", 62 | "nbconvert_exporter": "python", 63 | "pygments_lexer": "ipython3", 64 | "version": "3.7.1" 65 | } 66 | }, 67 | "nbformat": 4, 68 | "nbformat_minor": 2 69 | } 70 | -------------------------------------------------------------------------------- /js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bqplot-image-gl", 3 | "version": "1.4.2", 4 | "description": "An ipywidget image widget for astronomical purposes", 5 | "author": "Maarten A. Breddels", 6 | "main": "lib/index.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/glue-viz/bqplot-image-gl.git" 10 | }, 11 | "keywords": [ 12 | "jupyter", 13 | "widgets", 14 | "ipython", 15 | "ipywidgets", 16 | "jupyterlab-extension" 17 | ], 18 | "files": [ 19 | "lib/**/*.js", 20 | "dist/*.js", 21 | "shaders/*" 22 | ], 23 | "scripts": { 24 | "build": "npm run build:labextension && webpack --mode=production", 25 | "build:labextension": "jupyter labextension build .", 26 | "clean": "rimraf dist/", 27 | "prepare": "npm run build", 28 | "test": "echo \"Error: no test specified\" && exit 1", 29 | "watch": "run-p watch:nbextension watch:labextension", 30 | "watch:nbextension": "webpack --watch --mode=development", 31 | "watch:labextension": "jupyter labextension watch ." 32 | }, 33 | "devDependencies": { 34 | "@jupyterlab/builder": "^3.0.0-rc.4", 35 | "npm-run-all": "^4.1.5", 36 | "raw-loader": "~2.0.0", 37 | "rimraf": "^2.6.1", 38 | "webpack": "^4.35.0", 39 | "webpack-cli": "^3.0.8" 40 | }, 41 | "dependencies": { 42 | "@jupyter-widgets/base": "^1.0.0 || ^2.0.0 || ^3.0.0", 43 | "bqplot": "^0.5.3", 44 | "d3": "^5.7.0", 45 | "d3-color": "^1.4.0", 46 | "d3-contour": "^1.3.2", 47 | "d3-geo": "^1.11.6", 48 | "d3-selection": "^1", 49 | "is-typedarray": "^1.0.0", 50 | "jupyter-dataserializers": "^1.3.1", 51 | "lodash": "^4.17.4", 52 | "raw-loader": "~2.0.0", 53 | "three": "^0.97.0" 54 | }, 55 | "jupyterlab": { 56 | "extension": "lib/labplugin", 57 | "outputDir": "../share/jupyter/labextensions/bqplot-image-gl", 58 | "sharedPackages": { 59 | "@jupyter-widgets/base": { 60 | "bundled": false, 61 | "singleton": true 62 | }, 63 | "bqplot": { 64 | "bundled": false, 65 | "singleton": true 66 | }, 67 | "d3": { 68 | "bundled": false 69 | }, 70 | "d3-selection": { 71 | "bundled": false 72 | }, 73 | "threejs": { 74 | "bundled": false 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /js/lib/examples/lines/LineGeometry.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author WestLangley / http://github.com/WestLangley 3 | * 4 | */ 5 | 6 | var LineSegmentsGeometry = require('./LineSegmentsGeometry').LineSegmentsGeometry; 7 | 8 | 9 | var LineGeometry = function () { 10 | 11 | LineSegmentsGeometry.call( this ); 12 | 13 | this.type = 'LineGeometry'; 14 | 15 | }; 16 | 17 | LineGeometry.prototype = Object.assign( Object.create( LineSegmentsGeometry.prototype ), { 18 | 19 | constructor: LineGeometry, 20 | 21 | isLineGeometry: true, 22 | 23 | setPositions: function ( array ) { 24 | 25 | // converts [ x1, y1, z1, x2, y2, z2, ... ] to pairs format 26 | 27 | var length = array.length - 3; 28 | var points = new Float32Array( 2 * length ); 29 | 30 | for ( var i = 0; i < length; i += 3 ) { 31 | 32 | points[ 2 * i ] = array[ i ]; 33 | points[ 2 * i + 1 ] = array[ i + 1 ]; 34 | points[ 2 * i + 2 ] = array[ i + 2 ]; 35 | 36 | points[ 2 * i + 3 ] = array[ i + 3 ]; 37 | points[ 2 * i + 4 ] = array[ i + 4 ]; 38 | points[ 2 * i + 5 ] = array[ i + 5 ]; 39 | 40 | } 41 | 42 | LineSegmentsGeometry.prototype.setPositions.call( this, points ); 43 | 44 | return this; 45 | 46 | }, 47 | 48 | setColors: function ( array ) { 49 | 50 | // converts [ r1, g1, b1, r2, g2, b2, ... ] to pairs format 51 | 52 | var length = array.length - 3; 53 | var colors = new Float32Array( 2 * length ); 54 | 55 | for ( var i = 0; i < length; i += 3 ) { 56 | 57 | colors[ 2 * i ] = array[ i ]; 58 | colors[ 2 * i + 1 ] = array[ i + 1 ]; 59 | colors[ 2 * i + 2 ] = array[ i + 2 ]; 60 | 61 | colors[ 2 * i + 3 ] = array[ i + 3 ]; 62 | colors[ 2 * i + 4 ] = array[ i + 4 ]; 63 | colors[ 2 * i + 5 ] = array[ i + 5 ]; 64 | 65 | } 66 | 67 | LineSegmentsGeometry.prototype.setColors.call( this, colors ); 68 | 69 | return this; 70 | 71 | }, 72 | 73 | fromLine: function ( line ) { 74 | 75 | var geometry = line.geometry; 76 | 77 | if ( geometry.isGeometry ) { 78 | 79 | this.setPositions( geometry.vertices ); 80 | 81 | } else if ( geometry.isBufferGeometry ) { 82 | 83 | this.setPositions( geometry.position.array ); // assumes non-indexed 84 | 85 | } 86 | 87 | // set colors, maybe 88 | 89 | return this; 90 | 91 | }, 92 | 93 | copy: function ( source ) { 94 | 95 | // todo 96 | 97 | return this; 98 | 99 | } 100 | 101 | } ); 102 | 103 | module.exports = { 104 | LineGeometry: LineGeometry 105 | }; 106 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from os.path import join as pjoin 3 | from setuptools import setup, find_packages, Command 4 | import os 5 | 6 | from jupyter_packaging import ( 7 | create_cmdclass, 8 | install_npm, 9 | ensure_targets, 10 | combine_commands, 11 | get_version, 12 | skip_if_exists, 13 | ) 14 | 15 | LONG_DESCRIPTION = 'An ipywidget image widget for astronomical purposes' 16 | here = os.path.dirname(os.path.abspath(__file__)) 17 | name = 'bqplot-image-gl' 18 | package_name = name.replace('-', '_') 19 | version = get_version(pjoin(package_name, '_version.py')) 20 | 21 | js_dir = pjoin(here, 'js') 22 | 23 | # Representative files that should exist after a successful build 24 | jstargets = [ 25 | pjoin('share', 'jupyter', 'nbextensions', f'{name}', 'index.js'), 26 | # pjoin('share', 'jupyter', 'labextensions', f'{name}', 'package.json'), 27 | ] 28 | 29 | data_files_spec = [ 30 | (f'share/jupyter/nbextensions/{name}', f'share/jupyter/nbextensions/{name}', '*.js'), 31 | (f'share/jupyter/labextensions/{name}/', f'share/jupyter/labextensions/{name}/', '**'), 32 | (f'etc/jupyter/nbconfig/notebook.d', f'etc/jupyter/nbconfig/notebook.d', f'{name}.json'), 33 | ] 34 | 35 | js_command = combine_commands( 36 | install_npm(js_dir, build_dir='share/jupyter/', source_dir='js/src', build_cmd='build'), ensure_targets(jstargets), 37 | ) 38 | 39 | cmdclass = create_cmdclass('jsdeps', data_files_spec=data_files_spec) 40 | is_repo = os.path.exists(os.path.join(here, '.git')) 41 | if is_repo: 42 | cmdclass['jsdeps'] = js_command 43 | else: 44 | cmdclass['jsdeps'] = skip_if_exists(jstargets, js_command) 45 | 46 | setup( 47 | name=name, 48 | version=version, 49 | description='An ipywidget image widget for astronomical purposes', 50 | long_description=LONG_DESCRIPTION, 51 | include_package_data=True, 52 | install_requires=[ 53 | 'ipywidgets>=7.0.0', 54 | 'bqplot>=0.12' 55 | ], 56 | packages=find_packages(), 57 | zip_safe=False, 58 | cmdclass=cmdclass, 59 | author='Maarten A. Breddels', 60 | author_email='maartenbreddels@gmail.com', 61 | url='https://github.com/glue-viz/bqplot-image-gl', 62 | keywords=[ 63 | 'ipython', 64 | 'jupyter', 65 | 'widgets', 66 | ], 67 | classifiers=[ 68 | 'Development Status :: 4 - Beta', 69 | 'Framework :: IPython', 70 | 'Intended Audience :: Developers', 71 | 'Intended Audience :: Science/Research', 72 | 'Topic :: Multimedia :: Graphics', 73 | 'Programming Language :: Python :: 2', 74 | 'Programming Language :: Python :: 2.7', 75 | 'Programming Language :: Python :: 3', 76 | 'Programming Language :: Python :: 3.3', 77 | 'Programming Language :: Python :: 3.4', 78 | 'Programming Language :: Python :: 3.5', 79 | ], 80 | ) 81 | -------------------------------------------------------------------------------- /js/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | var version = require('./package.json').version; 4 | 5 | // Custom webpack rules are generally the same for all webpack bundles, hence 6 | // stored in a separate local variable. 7 | var rules = [ 8 | { test: /\.css$/, use: ['style-loader', 'css-loader']} 9 | ] 10 | 11 | 12 | module.exports = [ 13 | {// Notebook extension 14 | // 15 | // This bundle only contains the part of the JavaScript that is run on 16 | // load of the notebook. This section generally only performs 17 | // some configuration for requirejs, and provides the legacy 18 | // "load_ipython_extension" function which is required for any notebook 19 | // extension. 20 | // 21 | entry: './lib/extension.js', 22 | output: { 23 | filename: 'extension.js', 24 | path: path.resolve(__dirname, '../share/jupyter/nbextensions/bqplot-image-gl'), 25 | libraryTarget: 'amd', 26 | devtoolModuleFilenameTemplate: 'webpack://jupyter-widgets/bqplot-image-gl/[resource-path]?[loaders]', 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: './lib/index.js', 36 | output: { 37 | filename: 'index.js', 38 | path: path.resolve(__dirname, '../share/jupyter/nbextensions/bqplot-image-gl'), 39 | libraryTarget: 'amd', 40 | devtoolModuleFilenameTemplate: 'webpack://jupyter-widgets/bqplot-image-gl/[resource-path]?[loaders]', 41 | }, 42 | devtool: 'source-map', 43 | module: { 44 | rules: rules 45 | }, 46 | externals: ['@jupyter-widgets/base', 'bqplot'] 47 | }, 48 | {// Embeddable bqplot-image-gl bundle 49 | // 50 | // This bundle is generally almost identical to the notebook bundle 51 | // containing the custom widget views and models. 52 | // 53 | // The only difference is in the configuration of the webpack public path 54 | // for the static assets. 55 | // 56 | // It will be automatically distributed by unpkg to work with the static 57 | // widget embedder. 58 | // 59 | // The target bundle is always `dist/index.js`, which is the path required 60 | // by the custom widget embedder. 61 | // 62 | entry: './lib/embed.js', 63 | output: { 64 | filename: 'index.js', 65 | path: path.resolve(__dirname, 'dist'), 66 | libraryTarget: 'amd', 67 | publicPath: 'https://unpkg.com/bqplot-image-gl@' + version + '/dist/', 68 | devtoolModuleFilenameTemplate: 'webpack://jupyter-widgets/bqplot-image-gl/[resource-path]?[loaders]', 69 | }, 70 | devtool: 'source-map', 71 | module: { 72 | rules: rules 73 | }, 74 | externals: ['@jupyter-widgets/base', 'bqplot'] 75 | } 76 | ]; 77 | -------------------------------------------------------------------------------- /bqplot_image_gl/imagegl.py: -------------------------------------------------------------------------------- 1 | import ipywidgets as widgets 2 | import bqplot 3 | from traittypes import Array 4 | from bqplot.traits import (array_serialization, array_squeeze) 5 | from traitlets import Int, Unicode, List, Dict, Float, Instance 6 | from bqplot.marks import shape 7 | from bqplot.traits import array_to_json, array_from_json 8 | from bqplot_image_gl._version import __version__ 9 | 10 | __all__ = ['ImageGL', 'Contour'] 11 | 12 | 13 | @widgets.register 14 | class ImageGL(bqplot.Mark): 15 | """An example widget.""" 16 | _view_name = Unicode('ImageGLView').tag(sync=True) 17 | _model_name = Unicode('ImageGLModel').tag(sync=True) 18 | _view_module = Unicode('bqplot-image-gl').tag(sync=True) 19 | _model_module = Unicode('bqplot-image-gl').tag(sync=True) 20 | _view_module_version = Unicode('^' + __version__).tag(sync=True) 21 | _model_module_version = Unicode('^' + __version__).tag(sync=True) 22 | 23 | image = Array().tag(sync=True, 24 | scaled=True, 25 | rtype='Color', 26 | atype='bqplot.ColorAxis', 27 | **array_serialization) 28 | interpolation = Unicode('nearest', allow_none=True).tag(sync=True) 29 | opacity = Float(1.0).tag(sync=True) 30 | x = Array(default_value=(0, 1)).tag(sync=True, scaled=True, 31 | rtype='Number', 32 | atype='bqplot.Axis', 33 | **array_serialization)\ 34 | .valid(array_squeeze, shape(2)) 35 | y = Array(default_value=(0, 1)).tag(sync=True, scaled=True, 36 | rtype='Number', 37 | atype='bqplot.Axis', 38 | **array_serialization)\ 39 | .valid(array_squeeze, shape(2)) 40 | scales_metadata = Dict({ 41 | 'x': {'orientation': 'horizontal', 'dimension': 'x'}, 42 | 'y': {'orientation': 'vertical', 'dimension': 'y'}, 43 | 'image': {'dimension': 'color'}, 44 | }).tag(sync=True) 45 | 46 | 47 | def double_list_array_from_json(double_list): 48 | return [[array_from_json(k) for k in array_list] for array_list in double_list] 49 | 50 | 51 | def double_list_array_to_json(double_list, obj=None): 52 | return [[array_to_json(k) for k in array_list] for array_list in double_list] 53 | 54 | 55 | double_list_array_serialization = dict(to_json=double_list_array_to_json, 56 | from_json=double_list_array_from_json) 57 | 58 | 59 | @widgets.register 60 | class Contour(bqplot.Mark): 61 | _view_name = Unicode('ContourView').tag(sync=True) 62 | _model_name = Unicode('ContourModel').tag(sync=True) 63 | _view_module = Unicode('bqplot-image-gl').tag(sync=True) 64 | _model_module = Unicode('bqplot-image-gl').tag(sync=True) 65 | _view_module_version = Unicode('^' + __version__).tag(sync=True) 66 | _model_module_version = Unicode('^' + __version__).tag(sync=True) 67 | 68 | image = Instance(ImageGL, allow_none=True).tag(sync=True, **widgets.widget_serialization) 69 | label_steps = Int(40).tag(sync=True) 70 | contour_lines = (List(List(Array(None, allow_none=True))) 71 | .tag(sync=True, **double_list_array_serialization)) 72 | level = (Float() | List(Float())).tag(sync=True) 73 | label = (Unicode(None, allow_none=True) | List(Unicode())).tag(sync=True) 74 | color = (widgets.Color(None, allow_none=True) | 75 | List(widgets.Color(None, allow_none=True))).tag(sync=True) 76 | scales_metadata = Dict({ 77 | 'x': {'orientation': 'horizontal', 'dimension': 'x'}, 78 | 'y': {'orientation': 'vertical', 'dimension': 'y'}, 79 | }).tag(sync=True) 80 | -------------------------------------------------------------------------------- /examples/brush-ellipse.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": { 7 | "ExecuteTime": { 8 | "end_time": "2020-03-31T10:39:19.750785Z", 9 | "start_time": "2020-03-31T10:39:18.905864Z" 10 | } 11 | }, 12 | "outputs": [], 13 | "source": [ 14 | "import bqplot\n", 15 | "import bqplot.pyplot as plt\n", 16 | "from bqplot_image_gl.interacts import BrushEllipseSelector\n", 17 | "import numpy as np\n", 18 | "N = 1000\n", 19 | "x, y = np.random.normal(size=(2, N))\n", 20 | "scale_x = bqplot.LinearScale()\n", 21 | "scale_y = bqplot.LinearScale()" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": null, 27 | "metadata": { 28 | "ExecuteTime": { 29 | "end_time": "2020-03-31T10:39:19.815423Z", 30 | "start_time": "2020-03-31T10:39:19.753452Z" 31 | } 32 | }, 33 | "outputs": [], 34 | "source": [ 35 | "fig = plt.figure()\n", 36 | "s = plt.scatter(x, y, scales={'x': scale_x, 'y': scale_y})\n", 37 | "plt.show()" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": null, 43 | "metadata": { 44 | "ExecuteTime": { 45 | "end_time": "2020-03-31T10:37:06.528323Z", 46 | "start_time": "2020-03-31T10:37:06.523302Z" 47 | } 48 | }, 49 | "outputs": [], 50 | "source": [ 51 | "s.selected_style = {'fill': 'orange'}" 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": null, 57 | "metadata": { 58 | "ExecuteTime": { 59 | "end_time": "2020-03-31T10:37:07.676045Z", 60 | "start_time": "2020-03-31T10:37:07.664249Z" 61 | } 62 | }, 63 | "outputs": [], 64 | "source": [ 65 | "fig.interaction = BrushEllipseSelector(x_scale=scale_x, y_scale=scale_y, marks=[s])" 66 | ] 67 | }, 68 | { 69 | "cell_type": "code", 70 | "execution_count": null, 71 | "metadata": { 72 | "ExecuteTime": { 73 | "end_time": "2020-03-31T10:37:01.946734Z", 74 | "start_time": "2020-03-31T10:37:01.942021Z" 75 | } 76 | }, 77 | "outputs": [], 78 | "source": [ 79 | "fig.interaction.selected_x = [-2, -0.5]\n", 80 | "fig.interaction.selected_y = [1, 2]" 81 | ] 82 | }, 83 | { 84 | "cell_type": "code", 85 | "execution_count": null, 86 | "metadata": { 87 | "ExecuteTime": { 88 | "end_time": "2020-03-31T10:36:47.441439Z", 89 | "start_time": "2020-03-31T10:36:47.438240Z" 90 | } 91 | }, 92 | "outputs": [], 93 | "source": [ 94 | "fig.interaction.color = 'green'" 95 | ] 96 | }, 97 | { 98 | "cell_type": "code", 99 | "execution_count": null, 100 | "metadata": { 101 | "ExecuteTime": { 102 | "end_time": "2020-03-31T10:36:49.197336Z", 103 | "start_time": "2020-03-31T10:36:49.193708Z" 104 | } 105 | }, 106 | "outputs": [], 107 | "source": [ 108 | "fig.interaction.style = {'fill': 'blue'}" 109 | ] 110 | }, 111 | { 112 | "cell_type": "code", 113 | "execution_count": null, 114 | "metadata": { 115 | "ExecuteTime": { 116 | "end_time": "2020-03-31T10:36:54.148144Z", 117 | "start_time": "2020-03-31T10:36:54.144600Z" 118 | } 119 | }, 120 | "outputs": [], 121 | "source": [ 122 | "fig.interaction.border_style = {'stroke': 'blue'}" 123 | ] 124 | }, 125 | { 126 | "cell_type": "code", 127 | "execution_count": null, 128 | "metadata": { 129 | "ExecuteTime": { 130 | "end_time": "2020-03-31T10:10:25.273187Z", 131 | "start_time": "2020-03-31T10:10:25.270960Z" 132 | } 133 | }, 134 | "outputs": [], 135 | "source": [ 136 | "# fig.interaction = bqplot.interacts.BrushSelector(x_scale=scale_x, y_scale=scale_y, marks=[s])" 137 | ] 138 | } 139 | ], 140 | "metadata": { 141 | "kernelspec": { 142 | "display_name": "Python 3", 143 | "language": "python", 144 | "name": "python3" 145 | }, 146 | "language_info": { 147 | "codemirror_mode": { 148 | "name": "ipython", 149 | "version": 3 150 | }, 151 | "file_extension": ".py", 152 | "mimetype": "text/x-python", 153 | "name": "python", 154 | "nbconvert_exporter": "python", 155 | "pygments_lexer": "ipython3", 156 | "version": "3.7.3" 157 | } 158 | }, 159 | "nbformat": 4, 160 | "nbformat_minor": 2 161 | } 162 | -------------------------------------------------------------------------------- /js/lib/serialize.js: -------------------------------------------------------------------------------- 1 | /*jshint esversion: 6 */ 2 | 3 | var _ = require('underscore'); 4 | var isTypedArray = require('is-typedarray'); 5 | 6 | var typesToArray = { 7 | int8: Int8Array, 8 | int16: Int16Array, 9 | int32: Int32Array, 10 | uint8: Uint8Array, 11 | uint16: Uint16Array, 12 | uint32: Uint32Array, 13 | float32: Float32Array, 14 | float64: Float64Array 15 | }; 16 | 17 | var arrayToTypes = { 18 | Int8Array: 'int8', 19 | Int16Array: 'int16', 20 | Int32Array: 'int32', 21 | Uint8Array: 'uint8', 22 | Uint16Array: 'uint16', 23 | Uint32Array: 'uint32', 24 | Float32Array: 'float32', 25 | Float64Array: 'float64' 26 | }; 27 | 28 | 29 | function deserialize_typed_array(data, manager) { 30 | var type = typesToArray[data.dtype]; 31 | if(data == null) { 32 | console.log('data is null'); 33 | } 34 | if(!data.value) { 35 | console.log('data.buffer is null'); 36 | } 37 | if(!data.value.buffer) { 38 | console.log('data.buffer is null'); 39 | } 40 | var ar = new type(data.value.buffer); 41 | ar.type = data.type; 42 | if(data.shape && data.shape.length >= 2) { 43 | if(data.shape.length > 2) 44 | throw new Error("only arrays with rank 1 or 2 supported"); 45 | var offset = 0; 46 | var shape = data.shape; 47 | var arrays = []; 48 | // slice the 1d typed arrays in multiple arrays and put them in a 49 | // regular array 50 | for(var i=0; i < data.shape[0]; i++) { 51 | arrays.push(ar.slice(i*data.shape[1], (i+1)*data.shape[1])); 52 | } 53 | return arrays; 54 | } else { 55 | return ar; 56 | } 57 | } 58 | 59 | function serialize_typed_array(ar, manager) { 60 | if(ar == null) { 61 | console.log('data is null'); 62 | } 63 | if(!ar.buffer) { 64 | console.log('ar.buffer is null or not defined'); 65 | } 66 | var dtype = arrayToTypes[ar.constructor.name]; 67 | var type = ar.type || null; 68 | var wire = {dtype: dtype, value: new DataView(ar.buffer), shape: [ar.length], type: type}; 69 | return wire; 70 | } 71 | /* 72 | function deserialize_ndarray(data, manager) { 73 | if(data === null) 74 | return null; 75 | console.log('deserialize_ndarray') 76 | return ndarray(deserialize_typed_array(data, manager), data.shape); 77 | } 78 | 79 | function serialize_ndarray(data, manager) { 80 | if(data === null) 81 | return null; 82 | var ar = data; 83 | if(_.isArray(data) && !data.buffer) { // plain list of list 84 | var ar = require("ndarray-pack")(data) 85 | } 86 | var data_json = {'data': ar.data.buffer, dtype:arrayToTypes[ar.data.constructor.name], shape:ar.shape} 87 | return data_json; 88 | } 89 | 90 | */ 91 | 92 | function deserialize_array_or_json(data, manager) { 93 | if(data == null) 94 | return null; 95 | var value = null; 96 | if(_.isNumber(data)) { // plain number 97 | return data; 98 | } 99 | else if(_.isArray(data)) { 100 | if(data.length == 0) { 101 | arrays = []; 102 | } else { 103 | if(_.isArray(data[0])) { // 2d array 104 | value = _.map(data, function(data1d) { return deserialize_array_or_json(data1d, manager);}); 105 | } else { // it contains a plain array most likely 106 | value = data; 107 | } 108 | } 109 | } else if(data.value && data.dtype) { // binary data 110 | value = deserialize_typed_array(data); 111 | } else { 112 | console.error('not sure what the data is'); 113 | } 114 | return value; 115 | } 116 | 117 | function serialize_array_or_json(data, manager) { 118 | if(data == null) 119 | return null; 120 | if(_.isNumber(data)) { 121 | return data; // return numbers directly 122 | } else if(_.isArray(data)) { 123 | return data.map((ar) => serialize_array_or_json(ar, manager)); 124 | } else if(isTypedArray(data)) { 125 | return serialize_typed_array(data, manager); 126 | } 127 | } 128 | 129 | module.exports = { 130 | array_or_json: { deserialize: deserialize_array_or_json, serialize: serialize_array_or_json }, 131 | //ndarray: { deserialize: deserialize_ndarray, serialize: serialize_ndarray }, 132 | }; 133 | -------------------------------------------------------------------------------- /js/lib/ViewListener.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var version = require('./version').version; 4 | 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | const base = require("@jupyter-widgets/base"); 7 | 8 | class ViewListenerModel extends base.DOMWidgetModel { 9 | defaults() { 10 | return Object.assign({}, base.DOMWidgetModel.prototype.defaults(), { 11 | _model_name: "ViewListenerModel", 12 | _view_name: "ViewListener", 13 | _model_module: "bqplot-image-gl", 14 | _view_module: "bqplot-image-gl", 15 | _model_module_version: version, 16 | _view_module_version: version, 17 | widget: null, 18 | css_selector: null, 19 | view_data: {} 20 | }); 21 | } 22 | initialize(attributes, options) { 23 | super.initialize(attributes, options); 24 | this._cleanups = []; 25 | const bind = (widgetModel) => { 26 | // similar to ipyevents we use the _view_count to track when the views are changing 27 | const viewCount = widgetModel.get('_view_count'); 28 | if (! (typeof viewCount === "number")) { 29 | widgetModel.set('_view_count', Object.values(widgetModel.views).length) 30 | } 31 | this.listenTo(widgetModel, 'change:_view_count', this._updateViews) 32 | this._updateViews(); 33 | } 34 | bind(this.get('widget')); 35 | window.lastViewListenerModel = this; 36 | } 37 | async _getViews() { 38 | const widgetModel = this.get('widget'); 39 | const views = await Promise.all(Object.values(widgetModel.views)); 40 | return views; 41 | } 42 | async _updateViews() { 43 | // remove old listeners 44 | this._cleanups.forEach((c) => c()); 45 | 46 | const views = await this._getViews(); 47 | await Promise.all(views.map((view) => view.displayed)); 48 | await Promise.all(views.map((view) => view.layoutPromise)); 49 | 50 | this.set('view_data', {}) // clear data 51 | const selector = this.get('css_selector'); 52 | // initial fill 53 | this._updateViewData(); 54 | 55 | // listen to element for resize events 56 | views.forEach((view) => { 57 | const resizeObserver = new ResizeObserver(entries => { 58 | this._updateViewData(); 59 | }); 60 | let el = view.el; 61 | el = selector ? el.querySelector(selector) : el; 62 | if(el) { 63 | resizeObserver.observe(el); 64 | const cleanup = () => resizeObserver.disconnect(); 65 | this._cleanups.push(cleanup) 66 | } else { 67 | console.error('could not find element with css selector', selector); 68 | } 69 | }) 70 | } 71 | async _updateViewData() { 72 | const views = await this._getViews(); 73 | const selector = this.get('css_selector'); 74 | const view_data = {} 75 | views.forEach((view) => { 76 | let el = view.el; 77 | el = selector ? el.querySelector(selector) : el; 78 | if(el) { 79 | const {x, y, width, height} = el.getBoundingClientRect(); 80 | view_data[view.cid] = {x, y, width, height}; 81 | } else { 82 | console.error('could not find element with css selector', selector); 83 | } 84 | }); 85 | this.set('view_data', view_data) 86 | this.save_changes(); 87 | } 88 | } 89 | 90 | ViewListenerModel.serializers = Object.assign({}, base.DOMWidgetModel.serializers, { widget: { deserialize: base.unpack_models } }); 91 | exports.ViewListenerModel = ViewListenerModel; 92 | 93 | class ViewListener extends base.DOMWidgetView { 94 | async render() { 95 | const result = await super.render(); 96 | this.renderJSON() 97 | this.model.on('change:view_data', this.renderJSON, this); 98 | window.lastViewListenerView = this; 99 | return result; 100 | } 101 | 102 | renderJSON() { 103 | const json = JSON.stringify(this.model.get('view_data'), null, 4); 104 | const viewCount = this.model.get('widget').get('_view_count'); 105 | this.el.innerHTML = `viewcount: ${viewCount}:
${json}
` 106 | } 107 | } 108 | exports.ViewListener = ViewListener; 109 | -------------------------------------------------------------------------------- /examples/lines.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "c88b886f", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import numpy as np\n", 11 | "# N = int(1e5)\n", 12 | "large = True \n", 13 | "if large:\n", 14 | " # for testing performance\n", 15 | " N = int(1e5)\n", 16 | " x = np.arange(N) * 100\n", 17 | " y = np.cumsum(np.random.random(N)*2-1)\n", 18 | " y -= y.mean()\n", 19 | "else:\n", 20 | " # for testing features\n", 21 | " x = np.arange(10) * 100\n", 22 | " y = np.array([0, 10, 4, 5, 6, 6, 6, 0, 10, 10])" 23 | ] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "execution_count": null, 28 | "id": "cd8f7a3e", 29 | "metadata": {}, 30 | "outputs": [], 31 | "source": [ 32 | "import numpy as np\n", 33 | "from bqplot import Figure, LinearScale, Axis, ColorScale, Lines\n", 34 | "from bqplot_image_gl import LinesGL\n", 35 | "import ipywidgets as widgets\n", 36 | "\n", 37 | "scale_x = LinearScale(min=-N*10, max=x.max()*1.2, allow_padding=False)\n", 38 | "scale_y = LinearScale(allow_padding=False)\n", 39 | "scales = {'x': scale_x, 'y': scale_y}\n", 40 | "axis_x = Axis(scale=scale_x, label='x')\n", 41 | "axis_y = Axis(scale=scale_y, label='y', orientation='vertical')\n", 42 | "line_gl = LinesGL(x=x, y=y, scales=scales, colors=['orange'])\n", 43 | "if large:\n", 44 | " marks = [line_gl]\n", 45 | "else:\n", 46 | " line = Lines(x=x, y=y+0.2, scales=scales)\n", 47 | " marks = [line_gl, line]\n", 48 | "figure = Figure(scales=scales, axes=[axis_x, axis_y], marks=marks)\n", 49 | "figure" 50 | ] 51 | }, 52 | { 53 | "cell_type": "code", 54 | "execution_count": null, 55 | "id": "acc86039", 56 | "metadata": {}, 57 | "outputs": [], 58 | "source": [ 59 | "figure.layout.width = \"1200px\"\n", 60 | "figure.layout.height = \"400px\"" 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": null, 66 | "id": "a5c1887f", 67 | "metadata": {}, 68 | "outputs": [], 69 | "source": [ 70 | "from bqplot import PanZoom\n", 71 | "panzoom = PanZoom(scales={'x': [scales['x']], 'y': [scales['y']]})\n", 72 | "figure.interaction = panzoom" 73 | ] 74 | }, 75 | { 76 | "cell_type": "code", 77 | "execution_count": null, 78 | "id": "77b44713", 79 | "metadata": {}, 80 | "outputs": [], 81 | "source": [ 82 | "slider = widgets.FloatLogSlider(min=-1, max=1.2, value=2)\n", 83 | "widgets.link((slider, 'value'), (line_gl, 'stroke_width'))\n", 84 | "if not large:\n", 85 | " widgets.link((slider, 'value'), (line, 'stroke_width'))\n", 86 | "slider" 87 | ] 88 | }, 89 | { 90 | "cell_type": "code", 91 | "execution_count": null, 92 | "id": "e2d6908a", 93 | "metadata": {}, 94 | "outputs": [], 95 | "source": [ 96 | "line_gl.colors = ['#f0f']" 97 | ] 98 | }, 99 | { 100 | "cell_type": "code", 101 | "execution_count": null, 102 | "id": "09f3be8b", 103 | "metadata": {}, 104 | "outputs": [], 105 | "source": [ 106 | "line_gl.y = -line_gl.y\n", 107 | "if not large:\n", 108 | " line.y = -line.y" 109 | ] 110 | }, 111 | { 112 | "cell_type": "code", 113 | "execution_count": null, 114 | "id": "b913a3c4", 115 | "metadata": {}, 116 | "outputs": [], 117 | "source": [ 118 | "line_gl.opacities = [0.2]\n", 119 | "if not large:\n", 120 | " line.opacities = [0.2]" 121 | ] 122 | }, 123 | { 124 | "cell_type": "code", 125 | "execution_count": null, 126 | "id": "fdb546b1", 127 | "metadata": {}, 128 | "outputs": [], 129 | "source": [ 130 | "line_gl.opacities = [0.8]\n", 131 | "if not large:\n", 132 | " line.opacities = [0.8]" 133 | ] 134 | }, 135 | { 136 | "cell_type": "code", 137 | "execution_count": null, 138 | "id": "3a9fd266", 139 | "metadata": {}, 140 | "outputs": [], 141 | "source": [] 142 | } 143 | ], 144 | "metadata": { 145 | "kernelspec": { 146 | "display_name": "Python 3", 147 | "language": "python", 148 | "name": "python3" 149 | }, 150 | "language_info": { 151 | "codemirror_mode": { 152 | "name": "ipython", 153 | "version": 3 154 | }, 155 | "file_extension": ".py", 156 | "mimetype": "text/x-python", 157 | "name": "python", 158 | "nbconvert_exporter": "python", 159 | "pygments_lexer": "ipython3", 160 | "version": "3.7.10" 161 | } 162 | }, 163 | "nbformat": 4, 164 | "nbformat_minor": 5 165 | } 166 | -------------------------------------------------------------------------------- /bqplot_image_gl/interacts.py: -------------------------------------------------------------------------------- 1 | from bqplot.interacts import BrushSelector, Interaction 2 | from bqplot.scales import Scale 3 | from traitlets import Float, Unicode, Dict, Instance, Int, List 4 | from ipywidgets.widgets.widget import widget_serialization 5 | from bqplot_image_gl._version import __version__ 6 | 7 | 8 | drag_events = ["dragstart", "dragmove", "dragend"] 9 | mouse_events = ['click', 'dblclick', 'mouseenter', 'mouseleave', 'contextmenu', 'mousemove'] 10 | keyboard_events = ['keydown', 'keyup'] 11 | 12 | 13 | class BrushEllipseSelector(BrushSelector): 14 | 15 | """BrushEllipse interval selector interaction. 16 | 17 | This 2-D selector interaction enables the user to select an ellipse 18 | region using the brushing action of the mouse. A mouse-down marks the 19 | center of the ellipse. The drag after the mouse down selects a point 20 | on the ellipse, drawn with the same aspect ratio as the change in x and y 21 | as measured in pixels. If pixel_aspect is set, the aspect ratio of the ellipse 22 | will be used instead. Note that the aspect ratio is respected in the view 23 | where the ellipse is drawn. 24 | 25 | Once an ellipse is drawn, it can be moved dragging, or reshaped by dragging 26 | the border. 27 | 28 | The selected_x and selected_y arrays define the bounding box of the ellipse. 29 | 30 | Attributes 31 | ---------- 32 | selected_x: numpy.ndarray 33 | Two element array containing the start and end of the interval selected 34 | in terms of the x_scale of the selector. 35 | This attribute changes while the selection is being made with the 36 | ``BrushSelector``. 37 | selected_y: numpy.ndarray 38 | Two element array containing the start and end of the interval selected 39 | in terms of the y_scale of the selector. 40 | This attribute changes while the selection is being made with the 41 | ``BrushEllipseSelector``. 42 | brushing: bool (default: False) 43 | boolean attribute to indicate if the selector is being dragged. 44 | It is True when the selector is being moved and False when it is not. 45 | This attribute can be used to trigger computationally intensive code 46 | which should be run only on the interval selection being completed as 47 | opposed to code which should be run whenever selected is changing. 48 | """ 49 | _view_module = Unicode('bqplot-image-gl').tag(sync=True) 50 | _model_module = Unicode('bqplot-image-gl').tag(sync=True) 51 | _view_module_version = Unicode('^' + __version__).tag(sync=True) 52 | _model_module_version = Unicode('^' + __version__).tag(sync=True) 53 | pixel_aspect = Float(None, allow_none=True).tag(sync=True) 54 | style = Dict({"opacity": 0.3, "cursor": "grab"}).tag(sync=True) 55 | border_style = Dict({"fill": "none", "stroke-width": "3px", 56 | "opacity": 0.3, "cursor": "col-resize"}).tag(sync=True) 57 | _view_name = Unicode('BrushEllipseSelector').tag(sync=True) 58 | _model_name = Unicode('BrushEllipseSelectorModel').tag(sync=True) 59 | 60 | 61 | class MouseInteraction(Interaction): 62 | """Mouse events listener. 63 | 64 | Listen for mouse events on the kernel side. 65 | The attributes 'x_scale' and 'y_scale' should be provided. 66 | 67 | Event being passed are 68 | * dragstart 69 | * dragmove 70 | * dragend 71 | * click 72 | * dblclick 73 | 74 | All events are passed by a custom event with the following spec: 75 | `{event: , pixel: {x: , y: }, domain: {x: , y: }} ` 76 | 77 | Pixel coordinates might be useful for debugging, domain coordinates should be used only. 78 | 79 | Attributes 80 | ---------- 81 | x_scale: An instance of Scale 82 | This is the scale which is used for inversion from the pixels to data 83 | co-ordinates in the x-direction. 84 | y_scale: An instance of Scale 85 | This is the scale which is used for inversion from the pixels to data 86 | co-ordinates in the y-direction. 87 | move_throttle: Send mouse move events only every specified milliseconds. 88 | """ 89 | _view_module = Unicode('bqplot-image-gl').tag(sync=True) 90 | _model_module = Unicode('bqplot-image-gl').tag(sync=True) 91 | _view_module_version = Unicode('^' + __version__).tag(sync=True) 92 | _model_module_version = Unicode('^' + __version__).tag(sync=True) 93 | _view_name = Unicode('MouseInteraction').tag(sync=True) 94 | _model_name = Unicode('MouseInteractionModel').tag(sync=True) 95 | x_scale = Instance(Scale, allow_none=True, default_value=None)\ 96 | .tag(sync=True, dimension='x', **widget_serialization) 97 | y_scale = Instance(Scale, allow_none=True, default_value=None)\ 98 | .tag(sync=True, dimension='y', **widget_serialization) 99 | cursor = Unicode('auto').tag(sync=True) 100 | move_throttle = Int(50).tag(sync=True) 101 | next = Instance(Interaction, allow_none=True).tag(sync=True, **widget_serialization) 102 | events = List(Unicode, default_value=drag_events + mouse_events + keyboard_events, 103 | allow_none=True).tag(sync=True) 104 | -------------------------------------------------------------------------------- /js/lib/examples/lines/LineSegmentsGeometry.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author WestLangley / http://github.com/WestLangley 3 | * 4 | */ 5 | 6 | var THREE = require('three'); 7 | 8 | var LineSegmentsGeometry = function () { 9 | 10 | THREE.InstancedBufferGeometry.call( this ); 11 | 12 | this.type = 'LineSegmentsGeometry'; 13 | 14 | var positions = [ - 1, 2, 0, 1, 2, 0, - 1, 1, 0, 1, 1, 0, - 1, 0, 0, 1, 0, 0, - 1, - 1, 0, 1, - 1, 0 ]; 15 | var uvs = [ - 1, 2, 1, 2, - 1, 1, 1, 1, - 1, - 1, 1, - 1, - 1, - 2, 1, - 2 ]; 16 | var index = [ 0, 2, 1, 2, 3, 1, 2, 4, 3, 4, 5, 3, 4, 6, 5, 6, 7, 5 ]; 17 | 18 | this.setIndex( index ); 19 | this.addAttribute( 'position', new THREE.Float32BufferAttribute( positions, 3 ) ); 20 | this.addAttribute( 'uv', new THREE.Float32BufferAttribute( uvs, 2 ) ); 21 | 22 | }; 23 | 24 | LineSegmentsGeometry.prototype = Object.assign( Object.create( THREE.InstancedBufferGeometry.prototype ), { 25 | 26 | constructor: LineSegmentsGeometry, 27 | 28 | isLineSegmentsGeometry: true, 29 | 30 | applyMatrix: function ( matrix ) { 31 | 32 | var start = this.attributes.instanceStart; 33 | var end = this.attributes.instanceEnd; 34 | 35 | if ( start !== undefined ) { 36 | 37 | matrix.applyToBufferAttribute( start ); 38 | 39 | matrix.applyToBufferAttribute( end ); 40 | 41 | start.data.needsUpdate = true; 42 | 43 | } 44 | 45 | if ( this.boundingBox !== null ) { 46 | 47 | this.computeBoundingBox(); 48 | 49 | } 50 | 51 | if ( this.boundingSphere !== null ) { 52 | 53 | this.computeBoundingSphere(); 54 | 55 | } 56 | 57 | return this; 58 | 59 | }, 60 | 61 | setPositions: function ( array ) { 62 | 63 | var lineSegments; 64 | 65 | if ( array instanceof Float32Array ) { 66 | 67 | lineSegments = array; 68 | 69 | } else if ( Array.isArray( array ) ) { 70 | 71 | lineSegments = new Float32Array( array ); 72 | 73 | } 74 | 75 | var instanceBuffer = new THREE.InstancedInterleavedBuffer( lineSegments, 6, 1 ); // xyz, xyz 76 | 77 | this.addAttribute( 'instanceStart', new THREE.InterleavedBufferAttribute( instanceBuffer, 3, 0 ) ); // xyz 78 | this.addAttribute( 'instanceEnd', new THREE.InterleavedBufferAttribute( instanceBuffer, 3, 3 ) ); // xyz 79 | 80 | // 81 | 82 | this.computeBoundingBox(); 83 | this.computeBoundingSphere(); 84 | 85 | return this; 86 | 87 | }, 88 | 89 | setColors: function ( array ) { 90 | 91 | var colors; 92 | 93 | if ( array instanceof Float32Array ) { 94 | 95 | colors = array; 96 | 97 | } else if ( Array.isArray( array ) ) { 98 | 99 | colors = new Float32Array( array ); 100 | 101 | } 102 | 103 | var instanceColorBuffer = new THREE.InstancedInterleavedBuffer( colors, 6, 1 ); // rgb, rgb 104 | 105 | this.addAttribute( 'instanceColorStart', new THREE.InterleavedBufferAttribute( instanceColorBuffer, 3, 0 ) ); // rgb 106 | this.addAttribute( 'instanceColorEnd', new THREE.InterleavedBufferAttribute( instanceColorBuffer, 3, 3 ) ); // rgb 107 | 108 | return this; 109 | 110 | }, 111 | 112 | fromWireframeGeometry: function ( geometry ) { 113 | 114 | this.setPositions( geometry.attributes.position.array ); 115 | 116 | return this; 117 | 118 | }, 119 | 120 | fromEdgesGeometry: function ( geometry ) { 121 | 122 | this.setPositions( geometry.attributes.position.array ); 123 | 124 | return this; 125 | 126 | }, 127 | 128 | fromLineSegements: function ( lineSegments ) { 129 | 130 | var geometry = lineSegments.geometry; 131 | 132 | if ( geometry.isGeometry ) { 133 | 134 | this.setPositions( geometry.vertices ); 135 | 136 | } else if ( geometry.isBufferGeometry ) { 137 | 138 | this.setPositions( geometry.position.array ); // assumes non-indexed 139 | 140 | } 141 | 142 | // set colors, maybe 143 | 144 | return this; 145 | 146 | }, 147 | 148 | computeBoundingBox: function () { 149 | 150 | var box = new THREE.Box3(); 151 | 152 | return function computeBoundingBox() { 153 | 154 | if ( this.boundingBox === null ) { 155 | 156 | this.boundingBox = new THREE.Box3(); 157 | 158 | } 159 | 160 | var start = this.attributes.instanceStart; 161 | var end = this.attributes.instanceEnd; 162 | 163 | if ( start !== undefined && end !== undefined ) { 164 | 165 | this.boundingBox.setFromBufferAttribute( start ); 166 | 167 | box.setFromBufferAttribute( end ); 168 | 169 | this.boundingBox.union( box ); 170 | 171 | } 172 | 173 | }; 174 | 175 | }(), 176 | 177 | computeBoundingSphere: function () { 178 | 179 | var vector = new THREE.Vector3(); 180 | 181 | return function computeBoundingSphere() { 182 | 183 | if ( this.boundingSphere === null ) { 184 | 185 | this.boundingSphere = new THREE.Sphere(); 186 | 187 | } 188 | 189 | if ( this.boundingBox === null ) { 190 | 191 | this.computeBoundingBox(); 192 | 193 | } 194 | 195 | var start = this.attributes.instanceStart; 196 | var end = this.attributes.instanceEnd; 197 | 198 | if ( start !== undefined && end !== undefined ) { 199 | 200 | var center = this.boundingSphere.center; 201 | 202 | this.boundingBox.getCenter( center ); 203 | 204 | var maxRadiusSq = 0; 205 | 206 | for ( var i = 0, il = start.count; i < il; i ++ ) { 207 | 208 | vector.fromBufferAttribute( start, i ); 209 | maxRadiusSq = Math.max( maxRadiusSq, center.distanceToSquared( vector ) ); 210 | 211 | vector.fromBufferAttribute( end, i ); 212 | maxRadiusSq = Math.max( maxRadiusSq, center.distanceToSquared( vector ) ); 213 | 214 | } 215 | 216 | this.boundingSphere.radius = Math.sqrt( maxRadiusSq ); 217 | 218 | if ( isNaN( this.boundingSphere.radius ) ) { 219 | 220 | console.error( 'LineSegmentsGeometry.computeBoundingSphere(): Computed radius is NaN. The instanced position data is likely to have NaN values.', this ); 221 | 222 | } 223 | 224 | } 225 | 226 | }; 227 | 228 | }(), 229 | 230 | toJSON: function () { 231 | 232 | // todo 233 | 234 | }, 235 | 236 | clone: function () { 237 | 238 | // todo 239 | 240 | }, 241 | 242 | copy: function ( source ) { 243 | 244 | // todo 245 | 246 | return this; 247 | 248 | } 249 | 250 | } ); 251 | 252 | module.exports = { 253 | LineSegmentsGeometry: LineSegmentsGeometry 254 | }; 255 | -------------------------------------------------------------------------------- /examples/mouse.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "discrete-retention", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import json\n", 11 | "import math\n", 12 | "import numpy as np" 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": null, 18 | "id": "ethical-posting", 19 | "metadata": {}, 20 | "outputs": [], 21 | "source": [ 22 | "with open('./data.json') as f:\n", 23 | " data = json.load(f)" 24 | ] 25 | }, 26 | { 27 | "cell_type": "code", 28 | "execution_count": null, 29 | "id": "emerging-marking", 30 | "metadata": {}, 31 | "outputs": [], 32 | "source": [ 33 | "values = np.array(data['values'], dtype='float32')\n", 34 | "values = values.reshape((data['height'], data['width']))[:10,]\n", 35 | "values.shape" 36 | ] 37 | }, 38 | { 39 | "cell_type": "code", 40 | "execution_count": null, 41 | "id": "twelve-cabinet", 42 | "metadata": {}, 43 | "outputs": [], 44 | "source": [ 45 | "import numpy as np\n", 46 | "from bqplot import Figure, LinearScale, Axis, ColorScale\n", 47 | "from bqplot_image_gl import ImageGL, Contour\n", 48 | "import ipywidgets as widgets\n", 49 | "scale_x = LinearScale(min=-1, max=4, allow_padding=False)\n", 50 | "scale_y = LinearScale(min=-1, max=4, allow_padding=False)\n", 51 | "scales = {'x': scale_x, 'y': scale_y}\n", 52 | "axis_x = Axis(scale=scale_x, label='x')\n", 53 | "axis_y = Axis(scale=scale_y, label='y', orientation='vertical')\n", 54 | "scales_image = {'x': scale_x, 'y': scale_y, 'image': ColorScale(min=np.min(values).item(), max=np.max(values).item())}\n" 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": null, 60 | "id": "appreciated-transformation", 61 | "metadata": {}, 62 | "outputs": [], 63 | "source": [ 64 | "figure = Figure(scales=scales, axes=[axis_x, axis_y])\n", 65 | "image = ImageGL(image=values, scales=scales_image, x=[0, 2], y=[0, 2])\n", 66 | "figure.marks = (image, )" 67 | ] 68 | }, 69 | { 70 | "cell_type": "code", 71 | "execution_count": null, 72 | "id": "buried-cooperation", 73 | "metadata": {}, 74 | "outputs": [], 75 | "source": [ 76 | "figure" 77 | ] 78 | }, 79 | { 80 | "cell_type": "code", 81 | "execution_count": null, 82 | "metadata": {}, 83 | "outputs": [], 84 | "source": [ 85 | "from bqplot_image_gl.interacts import MouseInteraction, keyboard_events, mouse_events\n", 86 | "from bqplot import PanZoom" 87 | ] 88 | }, 89 | { 90 | "cell_type": "code", 91 | "execution_count": null, 92 | "metadata": {}, 93 | "outputs": [], 94 | "source": [ 95 | "widget_label = widgets.Label(value=\"move cursor for information\")\n", 96 | "widget_label" 97 | ] 98 | }, 99 | { 100 | "cell_type": "code", 101 | "execution_count": null, 102 | "metadata": {}, 103 | "outputs": [], 104 | "source": [ 105 | "# if we want to work together with PanZoom, we don't want to processess drag events\n", 106 | "panzoom = PanZoom(scales={'x': [scales_image['x']], 'y': [scales_image['y']]})\n", 107 | "interaction = MouseInteraction(x_scale=scales_image['x'], y_scale=scales_image['y'], move_throttle=70, next=panzoom,\n", 108 | " events=keyboard_events + mouse_events)\n", 109 | "figure.interaction = interaction\n", 110 | "def on_mouse_msg(interaction, data, buffers):\n", 111 | " # it might be a good idea to throttle on the Python side as well, for instance when many computations\n", 112 | " # happen, we can effectively ignore the queue of messages\n", 113 | " if data['event'] == 'mousemove':\n", 114 | " domain_x = data['domain']['x']\n", 115 | " domain_y = data['domain']['y']\n", 116 | " normalized_x = (domain_x - image.x[0]) / (image.x[1] - image.x[0])\n", 117 | " normalized_y = (domain_y - image.y[0]) / (image.y[1] - image.y[0])\n", 118 | " # TODO: think about +/-1 and pixel edges\n", 119 | " pixel_x = int(math.floor(normalized_x * image.image.shape[1]))\n", 120 | " pixel_y = int(math.floor(normalized_y * image.image.shape[0]))\n", 121 | " if pixel_x >= 0 and pixel_x < image.image.shape[1] and pixel_y >= 0 and pixel_y < image.image.shape[0]:\n", 122 | " value = str(image.image[pixel_y, pixel_x])\n", 123 | " else:\n", 124 | " value = \"out of range\"\n", 125 | " msg = f\"x={pixel_x} y={pixel_y} value={value} (nx={normalized_x} ny={normalized_y})\"\n", 126 | " widget_label.value = msg\n", 127 | " elif data['event'] == 'mouseleave':\n", 128 | " widget_label.value = \"Bye!\"\n", 129 | " elif data['event'] == 'mouseenter':\n", 130 | " widget_label.value = \"Almost there...\" # this is is not visible because mousemove overwrites the msg\n", 131 | " else:\n", 132 | " widget_label.value = f'click {data}'\n", 133 | " \n", 134 | "interaction.on_msg(on_mouse_msg)" 135 | ] 136 | }, 137 | { 138 | "cell_type": "code", 139 | "execution_count": null, 140 | "metadata": {}, 141 | "outputs": [], 142 | "source": [ 143 | "# cherry pick particular events:\n", 144 | "# interaction.events = ['click']" 145 | ] 146 | }, 147 | { 148 | "cell_type": "code", 149 | "execution_count": null, 150 | "metadata": {}, 151 | "outputs": [], 152 | "source": [] 153 | } 154 | ], 155 | "metadata": { 156 | "kernelspec": { 157 | "display_name": "Python 3", 158 | "language": "python", 159 | "name": "python3" 160 | }, 161 | "language_info": { 162 | "codemirror_mode": { 163 | "name": "ipython", 164 | "version": 3 165 | }, 166 | "file_extension": ".py", 167 | "mimetype": "text/x-python", 168 | "name": "python", 169 | "nbconvert_exporter": "python", 170 | "pygments_lexer": "ipython3", 171 | "version": "3.8.6" 172 | }, 173 | "widgets": { 174 | "application/vnd.jupyter.widget-state+json": { 175 | "state": {}, 176 | "version_major": 2, 177 | "version_minor": 0 178 | } 179 | } 180 | }, 181 | "nbformat": 4, 182 | "nbformat_minor": 5 183 | } 184 | -------------------------------------------------------------------------------- /examples/viewlistener.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "7d2bb515", 6 | "metadata": {}, 7 | "source": [ 8 | "# ViewListener\n", 9 | "A ViewListener listens to all views of a widgets and will report the dimensions. An optional css selector can target specific DOM elements inside the view. The `ViewListener.view_data` dict contains the metadata for each view, and will be updated when a new view gets created, or the DOM elements in one of the views resizes." 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": 1, 15 | "id": "80d65a3d", 16 | "metadata": {}, 17 | "outputs": [ 18 | { 19 | "data": { 20 | "application/vnd.jupyter.widget-view+json": { 21 | "model_id": "7436208300ec46e1951461c54edb60cb", 22 | "version_major": 2, 23 | "version_minor": 0 24 | }, 25 | "text/plain": [ 26 | "FloatSlider(value=0.0)" 27 | ] 28 | }, 29 | "metadata": {}, 30 | "output_type": "display_data" 31 | } 32 | ], 33 | "source": [ 34 | "from bqplot_image_gl.viewlistener import ViewListener\n", 35 | "import ipywidgets as widgets\n", 36 | "slider = widgets.FloatSlider()\n", 37 | "slider" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": 2, 43 | "id": "69b7fab0", 44 | "metadata": {}, 45 | "outputs": [], 46 | "source": [ 47 | "vl = ViewListener(widget=slider, css_selector=\".ui-slider-handle\")" 48 | ] 49 | }, 50 | { 51 | "cell_type": "markdown", 52 | "id": "11fc8558", 53 | "metadata": {}, 54 | "source": [ 55 | "A viewlistener itself is a DOMWidget, which prints out the `view_data`." 56 | ] 57 | }, 58 | { 59 | "cell_type": "code", 60 | "execution_count": 3, 61 | "id": "11e7cca3", 62 | "metadata": {}, 63 | "outputs": [ 64 | { 65 | "data": { 66 | "application/vnd.jupyter.widget-view+json": { 67 | "model_id": "0ddd9abace884a3880dcf32ab07bbe01", 68 | "version_major": 2, 69 | "version_minor": 0 70 | }, 71 | "text/plain": [ 72 | "ViewListener(css_selector='.ui-slider-handle', widget=FloatSlider(value=0.0))" 73 | ] 74 | }, 75 | "metadata": {}, 76 | "output_type": "display_data" 77 | } 78 | ], 79 | "source": [ 80 | "vl" 81 | ] 82 | }, 83 | { 84 | "cell_type": "code", 85 | "execution_count": 4, 86 | "id": "c7dbbd9d", 87 | "metadata": {}, 88 | "outputs": [ 89 | { 90 | "data": { 91 | "text/plain": [ 92 | "{}" 93 | ] 94 | }, 95 | "execution_count": 4, 96 | "metadata": {}, 97 | "output_type": "execute_result" 98 | } 99 | ], 100 | "source": [ 101 | "vl.view_data" 102 | ] 103 | }, 104 | { 105 | "cell_type": "code", 106 | "execution_count": 5, 107 | "id": "6df31390", 108 | "metadata": {}, 109 | "outputs": [ 110 | { 111 | "data": { 112 | "application/vnd.jupyter.widget-view+json": { 113 | "model_id": "7436208300ec46e1951461c54edb60cb", 114 | "version_major": 2, 115 | "version_minor": 0 116 | }, 117 | "text/plain": [ 118 | "FloatSlider(value=0.0)" 119 | ] 120 | }, 121 | "metadata": {}, 122 | "output_type": "display_data" 123 | } 124 | ], 125 | "source": [ 126 | "slider" 127 | ] 128 | }, 129 | { 130 | "cell_type": "markdown", 131 | "id": "0a620ee5", 132 | "metadata": {}, 133 | "source": [ 134 | "If we modify the width of the slider handle, the `view_data` should be updated." 135 | ] 136 | }, 137 | { 138 | "cell_type": "code", 139 | "execution_count": 6, 140 | "id": "cd1fa27e", 141 | "metadata": {}, 142 | "outputs": [ 143 | { 144 | "data": { 145 | "text/html": [ 146 | "\n" 151 | ], 152 | "text/plain": [ 153 | "" 154 | ] 155 | }, 156 | "metadata": {}, 157 | "output_type": "display_data" 158 | } 159 | ], 160 | "source": [ 161 | "%%html\n", 162 | "" 167 | ] 168 | }, 169 | { 170 | "cell_type": "markdown", 171 | "id": "3fe77f7c", 172 | "metadata": {}, 173 | "source": [ 174 | "## Using bqplot" 175 | ] 176 | }, 177 | { 178 | "cell_type": "code", 179 | "execution_count": 7, 180 | "id": "103be845", 181 | "metadata": {}, 182 | "outputs": [ 183 | { 184 | "data": { 185 | "application/vnd.jupyter.widget-view+json": { 186 | "model_id": "b75a09d6642749e9bbc9d8923bc6eec0", 187 | "version_major": 2, 188 | "version_minor": 0 189 | }, 190 | "text/plain": [ 191 | "Figure(fig_margin={'top': 60, 'bottom': 60, 'left': 60, 'right': 60}, scale_x=LinearScale(allow_padding=False,…" 192 | ] 193 | }, 194 | "metadata": {}, 195 | "output_type": "display_data" 196 | } 197 | ], 198 | "source": [ 199 | "import bqplot\n", 200 | "# simple empty figure\n", 201 | "figure = bqplot.Figure()\n", 202 | "figure" 203 | ] 204 | }, 205 | { 206 | "cell_type": "code", 207 | "execution_count": 8, 208 | "id": "f82bf0b4", 209 | "metadata": {}, 210 | "outputs": [ 211 | { 212 | "data": { 213 | "application/vnd.jupyter.widget-view+json": { 214 | "model_id": "1e96464f5391421f9b86cf2b9b1ea202", 215 | "version_major": 2, 216 | "version_minor": 0 217 | }, 218 | "text/plain": [ 219 | "ViewListener(css_selector='.svg-figure > g', widget=Figure(fig_margin={'top': 60, 'bottom': 60, 'left': 60, 'r…" 220 | ] 221 | }, 222 | "metadata": {}, 223 | "output_type": "display_data" 224 | } 225 | ], 226 | "source": [ 227 | "# listen to the main bqplot canvas\n", 228 | "vl_figure = ViewListener(widget=figure, css_selector=\".svg-figure > g\")\n", 229 | "vl_figure" 230 | ] 231 | } 232 | ], 233 | "metadata": { 234 | "kernelspec": { 235 | "display_name": "Python 3", 236 | "language": "python", 237 | "name": "python3" 238 | }, 239 | "language_info": { 240 | "codemirror_mode": { 241 | "name": "ipython", 242 | "version": 3 243 | }, 244 | "file_extension": ".py", 245 | "mimetype": "text/x-python", 246 | "name": "python", 247 | "nbconvert_exporter": "python", 248 | "pygments_lexer": "ipython3", 249 | "version": "3.7.10" 250 | } 251 | }, 252 | "nbformat": 4, 253 | "nbformat_minor": 5 254 | } 255 | -------------------------------------------------------------------------------- /js/lib/linesgl.js: -------------------------------------------------------------------------------- 1 | var version = require('./version').version; 2 | var widgets = require('@jupyter-widgets/base'); 3 | var _ = require('lodash'); 4 | var d3 = require("d3"); 5 | var bqplot = require('bqplot'); 6 | var THREE = require('three'); 7 | var values = require('./values'); 8 | 9 | var Line2 = require('./examples/lines/Line2').Line2; 10 | var LineMaterial = require('./examples/lines/LineMaterial').LineMaterial; 11 | var LineGeometry = require('./examples/lines/LineGeometry').LineGeometry; 12 | 13 | 14 | const chunk_scales_extra = require('raw-loader!../shaders/scales-extra.glsl').default; 15 | const chunk_scales_transform = require('raw-loader!../shaders/scales-transform.glsl').default; 16 | 17 | const scaleTypeMap = { 18 | linear: 1, 19 | log: 2, 20 | }; 21 | 22 | class LinesGLModel extends bqplot.LinesModel { 23 | defaults() { 24 | return _.extend(bqplot.LinesModel.prototype.defaults(), { 25 | _model_name : 'LinesGLModel', 26 | _view_name : 'LinesGLView', 27 | _model_module : 'bqplot-image-gl', 28 | _view_module : 'bqplot-image-gl', 29 | _model_module_version : version, 30 | _view_module_version : version, 31 | }); 32 | } 33 | } 34 | 35 | class LinesGLView extends bqplot.Lines { 36 | 37 | async render() { 38 | this.uniforms = { 39 | domain_x : { type: "2f", value: [0., 1.] }, 40 | domain_y : { type: "2f", value: [0., 1.] }, 41 | range_x : { type: "2f", value: [0., 1.] }, 42 | range_y : { type: "2f", value: [0., 1.] }, 43 | diffuse: {type: '3f', value: [1, 0, 0]}, 44 | opacity: {type: 'f', value: 1.0}, 45 | } 46 | this.scale_defines = {} 47 | this.material = new LineMaterial(); 48 | this.uniforms = this.material.uniforms = {...this.material.uniforms, ...this.uniforms}; 49 | 50 | const result = await super.render(); 51 | window.lastLinesGLView = this; 52 | 53 | 54 | this.material.onBeforeCompile = (shader) => { 55 | // we include the scales header, and a snippet that uses the scales 56 | shader.vertexShader = "// added by bqplot-image-gl\n#include \n" + chunk_scales_extra + "// added by bqplot-image-gl\n" + shader.vertexShader; 57 | // we modify the shader to replace a piece 58 | const begin = 'vec4 start = modelViewMatrix * vec4( instanceStart, 1.0 );' 59 | const offset_begin = shader.vertexShader.indexOf(begin); 60 | if (offset_begin == -1) { 61 | console.error('Could not find magic begin line in shader'); 62 | } 63 | const end = 'vec4 end = modelViewMatrix * vec4( instanceEnd, 1.0 );'; 64 | const offset_end = shader.vertexShader.indexOf(end); 65 | if (offset_end == -1) { 66 | console.error('Could not find magic end line in shader'); 67 | } 68 | shader.vertexShader = shader.vertexShader.slice(0, offset_begin) + chunk_scales_transform + shader.vertexShader.slice(offset_end + end.length); 69 | }; 70 | this._updateMaterialScales(); 71 | this.update_stroke_width(); 72 | 73 | this.geometry = new LineGeometry(); 74 | this._updateGeometry(); 75 | this.line = new Line2(this.geometry, this.material); 76 | 77 | this.camera = new THREE.OrthographicCamera( 1 / - 2, 1 / 2, 1 / 2, 1 / - 2, -10000, 10000 ); 78 | this.camera.position.z = 10; 79 | this.camera.updateProjectionMatrix(); 80 | 81 | this.scene = new THREE.Scene(); 82 | this.scene.add(this.line); 83 | this.listenTo(this.model, 'change:x change:y', this._updateGeometry, this); 84 | 85 | return result; 86 | } 87 | _updateGeometry() { 88 | const scalar_names = ["x", "y", "z"]; 89 | const vector4_names = []; 90 | const get_value = (name, index, default_value) => { 91 | if (name === "z") { 92 | return 0; 93 | } 94 | return this.model.get(name); 95 | } 96 | const sequence_index = 0; // not used (see ipyvolume) 97 | const current = new values.Values(scalar_names, [], get_value, sequence_index, vector4_names); 98 | current.ensure_array('z') 99 | current.merge_to_vec3(["x", "y", "z"], "position"); 100 | // important to reset this, otherwise we may use an old buffered value 101 | // Note that if we upgrade threejs, this may be named differently https://github.com/mrdoob/three.js/issues/18990 102 | this.geometry.maxInstancedCount = undefined; 103 | this.geometry.setPositions(current.array_vec3['position']) 104 | 105 | } 106 | 107 | update_style() { 108 | const color = new THREE.Color(this.model.get('colors')[0]); 109 | this.material.color = color.toArray(); 110 | const opacities = this.model.get('opacities'); 111 | if(opacities && opacities.length) { 112 | this.uniforms['opacity'].value = opacities[0]; 113 | } else { 114 | this.uniforms['opacity'].value = 1.; 115 | } 116 | this.update_scene(); 117 | } 118 | 119 | update_stroke_width() { 120 | this.material.linewidth = this.model.get('stroke_width') 121 | this.update_scene(); 122 | } 123 | 124 | _updateMaterialScales() { 125 | const scales = {x: this.scales.x.model, y: this.scales.y.model} 126 | const new_scale_defines = {...this.scale_defines}; 127 | for (const key of Object.keys(scales)) { 128 | this.uniforms[`domain_${key}`].value = scales[key].domain; 129 | new_scale_defines[`SCALE_TYPE_${key}`] = scaleTypeMap[scales[key].type]; 130 | } 131 | if (!_.isEqual(this.scale_defines, new_scale_defines) ) { 132 | this.scale_defines = new_scale_defines; 133 | this._updateMaterials(); 134 | } 135 | } 136 | 137 | _updateMaterials() { 138 | this.material.defines = {...this.scale_defines, USE_SCALE_X: true, USE_SCALE_Y: true}; 139 | this.material.needsUpdate = true; 140 | } 141 | 142 | update_line_xy() { 143 | // called when the scales are changing 144 | this._updateMaterialScales(); 145 | this.update_scene(); 146 | } 147 | 148 | 149 | render_gl() { 150 | var fig = this.parent; 151 | var renderer = fig.renderer; 152 | this.camera.left = 0; 153 | this.camera.right = fig.plotarea_width; 154 | this.camera.bottom = 0; 155 | this.camera.top = fig.plotarea_height; 156 | this.camera.updateProjectionMatrix(); 157 | 158 | const x_scale = this.scales.x ? this.scales.x : this.parent.scale_x; 159 | const y_scale = this.scales.y ? this.scales.y : this.parent.scale_y; 160 | const range_x = this.parent.padded_range('x', x_scale.model); 161 | const range_y = this.parent.padded_range('y', y_scale.model); 162 | this.uniforms[`range_x`].value = range_x; 163 | this.uniforms['resolution'].value = [fig.plotarea_width, fig.plotarea_height]; 164 | this.uniforms[`range_y`].value = [range_y[1], range_y[0]]; // flipped coordinates in WebGL 165 | renderer.render(this.scene, this.camera); 166 | } 167 | 168 | update_scene(animate) { 169 | this.parent.update_gl(); 170 | } 171 | 172 | relayout() { 173 | this.update_scene(); 174 | } 175 | 176 | draw(animate) { 177 | this.set_ranges(); 178 | this.update_line_xy(animate); 179 | this.update_style(); 180 | } 181 | } 182 | 183 | export { 184 | LinesGLModel, LinesGLView 185 | }; 186 | -------------------------------------------------------------------------------- /js/lib/examples/lines/LineMaterial.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author WestLangley / http://github.com/WestLangley 3 | * 4 | * parameters = { 5 | * color: , 6 | * linewidth: , 7 | * dashed: , 8 | * dashScale: , 9 | * dashSize: , 10 | * gapSize: , 11 | * resolution: , // to be set by renderer 12 | * } 13 | */ 14 | 15 | var THREE = require('three'); 16 | 17 | lineUniforms = { 18 | 19 | linewidth: { value: 1 }, 20 | resolution: { value: new THREE.Vector2( 1, 1 ) }, 21 | dashScale: { value: 1 }, 22 | dashSize: { value: 1 }, 23 | gapSize: { value: 1 } // todo FIX - maybe change to totalSize 24 | 25 | }; 26 | 27 | lineShaders = { 28 | 29 | uniforms: THREE.UniformsUtils.merge( [ 30 | THREE.UniformsLib.common, 31 | THREE.UniformsLib.fog, 32 | lineUniforms 33 | ] ), 34 | 35 | vertexShader: 36 | ` 37 | #include 38 | #include 39 | #include 40 | #include 41 | #include 42 | 43 | uniform float linewidth; 44 | uniform vec2 resolution; 45 | 46 | attribute vec3 instanceStart; 47 | attribute vec3 instanceEnd; 48 | 49 | attribute vec3 instanceColorStart; 50 | attribute vec3 instanceColorEnd; 51 | 52 | varying vec2 vUv; 53 | 54 | #ifdef USE_DASH 55 | 56 | uniform float dashScale; 57 | attribute float instanceDistanceStart; 58 | attribute float instanceDistanceEnd; 59 | varying float vLineDistance; 60 | 61 | #endif 62 | 63 | void trimSegment( const in vec4 start, inout vec4 end ) { 64 | 65 | // trim end segment so it terminates between the camera plane and the near plane 66 | 67 | // conservative estimate of the near plane 68 | float a = projectionMatrix[ 2 ][ 2 ]; // 3nd entry in 3th column 69 | float b = projectionMatrix[ 3 ][ 2 ]; // 3nd entry in 4th column 70 | float nearEstimate = - 0.5 * b / a; 71 | 72 | float alpha = ( nearEstimate - start.z ) / ( end.z - start.z ); 73 | 74 | end.xyz = mix( start.xyz, end.xyz, alpha ); 75 | 76 | } 77 | 78 | void main() { 79 | 80 | #ifdef USE_COLOR 81 | 82 | vColor.xyz = ( position.y < 0.5 ) ? instanceColorStart : instanceColorEnd; 83 | 84 | #endif 85 | 86 | #ifdef USE_DASH 87 | 88 | vLineDistance = ( position.y < 0.5 ) ? dashScale * instanceDistanceStart : dashScale * instanceDistanceEnd; 89 | 90 | #endif 91 | 92 | float aspect = resolution.x / resolution.y; 93 | 94 | vUv = uv; 95 | 96 | // camera space 97 | vec4 start = modelViewMatrix * vec4( instanceStart, 1.0 ); 98 | vec4 end = modelViewMatrix * vec4( instanceEnd, 1.0 ); 99 | 100 | // special case for perspective projection, and segments that terminate either in, or behind, the camera plane 101 | // clearly the gpu firmware has a way of addressing this issue when projecting into ndc space 102 | // but we need to perform ndc-space calculations in the shader, so we must address this issue directly 103 | // perhaps there is a more elegant solution -- WestLangley 104 | 105 | bool perspective = ( projectionMatrix[ 2 ][ 3 ] == - 1.0 ); // 4th entry in the 3rd column 106 | 107 | if ( perspective ) { 108 | 109 | if ( start.z < 0.0 && end.z >= 0.0 ) { 110 | 111 | trimSegment( start, end ); 112 | 113 | } else if ( end.z < 0.0 && start.z >= 0.0 ) { 114 | 115 | trimSegment( end, start ); 116 | 117 | } 118 | 119 | } 120 | 121 | // clip space 122 | vec4 clipStart = projectionMatrix * start; 123 | vec4 clipEnd = projectionMatrix * end; 124 | 125 | // ndc space 126 | vec2 ndcStart = clipStart.xy / clipStart.w; 127 | vec2 ndcEnd = clipEnd.xy / clipEnd.w; 128 | 129 | // direction 130 | vec2 dir = ndcEnd - ndcStart; 131 | 132 | // account for clip-space aspect ratio 133 | dir.x *= aspect; 134 | dir = normalize( dir ); 135 | 136 | // perpendicular to dir 137 | vec2 offset = vec2( dir.y, - dir.x ); 138 | 139 | // undo aspect ratio adjustment 140 | dir.x /= aspect; 141 | offset.x /= aspect; 142 | 143 | // sign flip 144 | if ( position.x < 0.0 ) offset *= - 1.0; 145 | 146 | // endcaps 147 | if ( position.y < 0.0 ) { 148 | 149 | offset += - dir; 150 | 151 | } else if ( position.y > 1.0 ) { 152 | 153 | offset += dir; 154 | 155 | } 156 | 157 | // adjust for linewidth 158 | offset *= linewidth; 159 | 160 | // adjust for clip-space to screen-space conversion // maybe resolution should be based on viewport ... 161 | offset /= resolution.y; 162 | 163 | // select end 164 | vec4 clip = ( position.y < 0.5 ) ? clipStart : clipEnd; 165 | 166 | // back to clip space 167 | offset *= clip.w; 168 | 169 | clip.xy += offset; 170 | 171 | gl_Position = clip; 172 | 173 | vec4 mvPosition = ( position.y < 0.5 ) ? start : end; // this is an approximation 174 | 175 | #include 176 | #include 177 | #include 178 | 179 | } 180 | `, 181 | 182 | fragmentShader: 183 | ` 184 | uniform vec3 diffuse; 185 | uniform float opacity; 186 | 187 | #ifdef USE_DASH 188 | 189 | uniform float dashSize; 190 | uniform float gapSize; 191 | 192 | #endif 193 | 194 | varying float vLineDistance; 195 | 196 | #include 197 | #include 198 | #include 199 | #include 200 | #include 201 | 202 | varying vec2 vUv; 203 | 204 | void main() { 205 | 206 | #include 207 | 208 | #ifdef USE_DASH 209 | 210 | if ( vUv.y < - 1.0 || vUv.y > 1.0 ) discard; // discard endcaps 211 | 212 | if ( mod( vLineDistance, dashSize + gapSize ) > dashSize ) discard; // todo - FIX 213 | 214 | #endif 215 | 216 | if ( abs( vUv.y ) > 1.0 ) { 217 | 218 | float a = vUv.x; 219 | float b = ( vUv.y > 0.0 ) ? vUv.y - 1.0 : vUv.y + 1.0; 220 | float len2 = a * a + b * b; 221 | 222 | if ( len2 > 1.0 ) discard; 223 | 224 | } 225 | 226 | vec4 diffuseColor = vec4( diffuse, opacity ); 227 | 228 | #include 229 | #include 230 | 231 | gl_FragColor = vec4( diffuseColor.rgb, diffuseColor.a ); 232 | 233 | #include 234 | #include 235 | #include 236 | #include 237 | 238 | } 239 | ` 240 | }; 241 | 242 | 243 | var LineMaterial = function ( parameters ) { 244 | 245 | THREE.ShaderMaterial.call( this, { 246 | 247 | type: 'LineMaterial', 248 | 249 | uniforms: THREE.UniformsUtils.clone( lineShaders.uniforms ), 250 | 251 | vertexShader: lineShaders.vertexShader, 252 | fragmentShader: lineShaders.fragmentShader 253 | 254 | } ); 255 | 256 | this.dashed = false; 257 | 258 | Object.defineProperties( this, { 259 | 260 | color: { 261 | 262 | enumerable: true, 263 | 264 | get: function () { 265 | 266 | return this.uniforms.diffuse.value; 267 | 268 | }, 269 | 270 | set: function ( value ) { 271 | 272 | this.uniforms.diffuse.value = value; 273 | 274 | } 275 | 276 | }, 277 | 278 | linewidth: { 279 | 280 | enumerable: true, 281 | 282 | get: function () { 283 | 284 | return this.uniforms.linewidth.value; 285 | 286 | }, 287 | 288 | set: function ( value ) { 289 | 290 | this.uniforms.linewidth.value = value; 291 | 292 | } 293 | 294 | }, 295 | 296 | dashScale: { 297 | 298 | enumerable: true, 299 | 300 | get: function () { 301 | 302 | return this.uniforms.dashScale.value; 303 | 304 | }, 305 | 306 | set: function ( value ) { 307 | 308 | this.uniforms.dashScale.value = value; 309 | 310 | } 311 | 312 | }, 313 | 314 | dashSize: { 315 | 316 | enumerable: true, 317 | 318 | get: function () { 319 | 320 | return this.uniforms.dashSize.value; 321 | 322 | }, 323 | 324 | set: function ( value ) { 325 | 326 | this.uniforms.dashSize.value = value; 327 | 328 | } 329 | 330 | }, 331 | 332 | gapSize: { 333 | 334 | enumerable: true, 335 | 336 | get: function () { 337 | 338 | return this.uniforms.gapSize.value; 339 | 340 | }, 341 | 342 | set: function ( value ) { 343 | 344 | this.uniforms.gapSize.value = value; 345 | 346 | } 347 | 348 | }, 349 | 350 | resolution: { 351 | 352 | enumerable: true, 353 | 354 | get: function () { 355 | 356 | return this.uniforms.resolution.value; 357 | 358 | }, 359 | 360 | set: function ( value ) { 361 | 362 | this.uniforms.resolution.value.copy( value ); 363 | 364 | } 365 | 366 | } 367 | 368 | } ); 369 | 370 | this.setValues( parameters ); 371 | 372 | }; 373 | 374 | LineMaterial.prototype = Object.create( THREE.ShaderMaterial.prototype ); 375 | LineMaterial.prototype.constructor = LineMaterial; 376 | 377 | LineMaterial.prototype.isLineMaterial = true; 378 | 379 | LineMaterial.prototype.copy = function ( source ) { 380 | 381 | THREE.ShaderMaterial.prototype.copy.call( this, source ); 382 | 383 | this.color.copy( source.color ); 384 | 385 | this.linewidth = source.linewidth; 386 | 387 | this.resolution = source.resolution; 388 | 389 | this.dashScale = source.dashScale; 390 | 391 | this.dashSize = source.dashSize; 392 | 393 | this.gapSize = source.gapSize; 394 | 395 | return this; 396 | 397 | }; 398 | 399 | 400 | module.exports = { 401 | LineMaterial: LineMaterial 402 | }; 403 | -------------------------------------------------------------------------------- /js/lib/MouseInteraction.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var version = require('./version').version; 4 | 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | const Interaction_1 = require("bqplot"); 7 | const base_1 = require("@jupyter-widgets/base"); 8 | const d3 = require("d3"); 9 | const d3_drag_1 = require("d3-drag"); 10 | const _ = require("lodash"); 11 | const d3_selection_1 = require("d3-selection"); 12 | const d3GetEvent = function () { return require("d3-selection").event || window.event; }.bind(this); 13 | 14 | const clickEvents = ['click', 'dblclick', 'mouseenter', 'mouseleave', 'contextmenu']; 15 | const keyEvents = ['keydown', 'keyup']; 16 | const throttledEvents = ['mousemove']; 17 | const dragEvents = ['start', 'drag', 'end']; 18 | 19 | class MouseInteractionModel extends base_1.WidgetModel { 20 | defaults() { 21 | return Object.assign({}, base_1.WidgetModel.prototype.defaults(), { 22 | _model_name: "MouseInteractionModel", 23 | _view_name: "MouseInteraction", 24 | _model_module: "bqplot-image-gl", 25 | _view_module: "bqplot-image-gl", 26 | _model_module_version: version, 27 | _view_module_version: version, 28 | scale_x: null, 29 | scale_y: null, 30 | scale_y: null, 31 | move_throttle: 50, 32 | cursor: 'auto', 33 | next: null, 34 | events: [], 35 | }); 36 | } 37 | } 38 | 39 | MouseInteractionModel.serializers = Object.assign({}, base_1.WidgetModel.serializers, { x_scale: { deserialize: base_1.unpack_models }, y_scale: { deserialize: base_1.unpack_models }, next: { deserialize: base_1.unpack_models } }); 40 | exports.MouseInteractionModel = MouseInteractionModel; 41 | class MouseInteraction extends Interaction_1.Interaction { 42 | async render() { 43 | super.render(); 44 | this.eventElement = d3.select(this.parent.interaction.node()); 45 | this.nextView = null; 46 | this.x_scale = await this.create_child_view(this.model.get("x_scale")); 47 | this.y_scale = await this.create_child_view(this.model.get("y_scale")); 48 | this.last_mouse_point = [-1, -1]; 49 | this.parent.on("margin_updated", this.updateScaleRanges, this); 50 | this.updateScaleRanges(); 51 | const updateCursor = () => { 52 | this.eventElement.node().style.cursor = this.model.get('cursor'); 53 | }; 54 | this.listenTo(this.model, "change:cursor", updateCursor); 55 | this.listenTo(this.model, "change:next", this.updateNextInteract); 56 | updateCursor(); 57 | const updateThrottle = () => { 58 | this._emitThrottled = _.throttle(this._emit, this.model.get('move_throttle')); 59 | } 60 | updateThrottle(); 61 | this.listenTo(this.model, 'change:move_throttle', updateThrottle); 62 | this.listenTo(this.model, 'change:events', () => { 63 | this.unbindEvents(); 64 | this.bindEvents(); 65 | }); 66 | 67 | this.bindEvents(); 68 | // no await for this async function, because otherwise we want for 69 | // this.displayed, which will never happen before render resolves 70 | this.updateNextInteract(); 71 | } 72 | 73 | bindEvents() { 74 | const events = this.model.get("events"); 75 | // we don't want to bind these events if we don't need them, because drag events 76 | // can call stop propagation 77 | if (this.eventEnabled("dragstart") || this.eventEnabled("dragmove") || this.eventEnabled("dragend")) { 78 | this.eventElement.call(d3_drag_1.drag().on(this._eventName("start"), () => { 79 | const e = d3GetEvent(); 80 | this._emit('dragstart', { x: e.x, y: e.y }); 81 | }).on(this._eventName("drag"), () => { 82 | const e = d3GetEvent(); 83 | this._emit('dragmove', { x: e.x, y: e.y }); 84 | }).on(this._eventName("end"), () => { 85 | const e = d3GetEvent(); 86 | this._emit('dragend', { x: e.x, y: e.y }); 87 | })); 88 | } 89 | // and click events 90 | clickEvents.forEach(eventName => { 91 | this.eventElement.on(this._eventName(eventName), () => { 92 | this._emitThrottled.flush(); // we don't want mousemove events to come after enter/leave 93 | if (eventName !== 'mouseleave') { 94 | // to allow the div to get focus, but we will not allow it to be reachable by tab key 95 | this.parent.el.setAttribute("tabindex", -1); 96 | // we only get keyboard events if we have focus 97 | this.parent.el.focus({ preventScroll: true }); 98 | } 99 | if (eventName === 'mouseleave') { 100 | // restore 101 | this.parent.el.removeAttribute("tabindex"); 102 | } 103 | const e = d3GetEvent(); 104 | // to be consistent with drag events, we need to user clientPoint 105 | const [x, y] = d3_selection_1.clientPoint(this.eventElement.node(), e); 106 | const events = this.model.get("events"); 107 | if (eventName == 'contextmenu' && this.eventEnabled('contextmenu')) { 108 | e.preventDefault(); 109 | e.stopPropagation(); 110 | } 111 | this._emit(eventName, { x, y }, {button: e.button, altKey: e.altKey, ctrlKey: e.ctrlKey, metaKey: e.metaKey}); 112 | return false 113 | }); 114 | }); 115 | keyEvents.forEach(eventName => { 116 | d3.select(this.parent.el).on(this._eventName(eventName), () => { 117 | this._emitThrottled.flush(); // we don't want mousemove events to come after enter/leave 118 | const e = d3GetEvent(); 119 | // to be consistent with drag events, we need to user clientPoint 120 | // const [x, y] = d3_selection_1.clientPoint(eventElement.node(), e); 121 | const [x, y] = this.last_mouse_point; 122 | e.preventDefault(); 123 | e.stopPropagation(); 124 | this._emit(eventName, { x, y }, {code: e.code, charCode: e.charCode, key: e.key, keyCode: e.keyCode, altKey: e.altKey, ctrlKey: e.ctrlKey, metaKey: e.metaKey}); 125 | return false 126 | }); 127 | }); 128 | // throttled events 129 | throttledEvents.forEach(eventName => { 130 | this.eventElement.on(this._eventName(eventName), () => { 131 | const e = d3GetEvent(); 132 | // to be consistent with drag events, we need to user clientPoint 133 | const [x, y] = d3_selection_1.clientPoint(this.eventElement.node(), e); 134 | this.last_mouse_point = [x, y]; 135 | this._emitThrottled(eventName, { x, y }); 136 | }); 137 | }); 138 | } 139 | 140 | _eventName(name) { 141 | // using namespaced event names (e.g. click.view123) to support multiple 142 | // listeners on the same DOM element (our parent interaction node) 143 | return `${name}.${this.cid}` 144 | } 145 | 146 | unbindEvents() { 147 | const off = (name) => this.eventElement.on(this._eventName(name), null); 148 | clickEvents.forEach(off); 149 | keyEvents.forEach(off); 150 | throttledEvents.forEach(off); 151 | dragEvents.forEach(off); 152 | } 153 | 154 | eventEnabled(eventName) { 155 | const events = this.model.get("events"); 156 | return (events == null) || events.includes(eventName); 157 | } 158 | 159 | async updateNextInteract() { 160 | // this mimics Figure.set_iteraction 161 | // but we want the 'next' interaction to be added after we are added 162 | // to the DOM, so we don't steal all mouse events 163 | await this.displayed; 164 | const next = this.model.get('next') 165 | if(this.nextView) { 166 | this.nextView.remove(); 167 | } 168 | if(!next) { 169 | return; 170 | } 171 | this.nextView = await this.parent.create_child_view(next); 172 | this.parent.interaction.node().appendChild(this.nextView.el); 173 | this.parent.displayed.then(() => { 174 | this.nextView.trigger("displayed"); 175 | }); 176 | } 177 | 178 | updateScaleRanges() { 179 | this.x_scale.set_range(this.parent.padded_range("x", this.x_scale.model)); 180 | this.y_scale.set_range(this.parent.padded_range("y", this.y_scale.model)); 181 | } 182 | remove() { 183 | super.remove(); 184 | if(this.nextView) { 185 | this.nextView.remove(); 186 | } 187 | this.unbindEvents(); 188 | this.parent.off('margin_updated', this.updateScaleRanges); 189 | this.parent.el.removeAttribute("tabindex"); 190 | this._emitThrottled.flush(); 191 | } 192 | _emit(name, { x, y }, extra) { 193 | if(!this.eventEnabled(name)) { 194 | return; 195 | } 196 | let domain = { x: this.x_scale.scale.invert(x), y: this.y_scale.scale.invert(y) }; 197 | this.send({ event: name, pixel: { x, y }, domain: domain, ...extra }); 198 | } 199 | } 200 | exports.MouseInteraction = MouseInteraction; 201 | -------------------------------------------------------------------------------- /examples/contour.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import json\n", 10 | "import numpy as np" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": null, 16 | "metadata": {}, 17 | "outputs": [], 18 | "source": [ 19 | "with open('./data.json') as f:\n", 20 | " data = json.load(f)" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": null, 26 | "metadata": {}, 27 | "outputs": [], 28 | "source": [ 29 | "values = np.array(data['values'], dtype='float32')\n", 30 | "values = values.reshape((data['height'], data['width']))\n", 31 | "values" 32 | ] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": null, 37 | "metadata": {}, 38 | "outputs": [], 39 | "source": [ 40 | "values.shape" 41 | ] 42 | }, 43 | { 44 | "cell_type": "code", 45 | "execution_count": null, 46 | "metadata": {}, 47 | "outputs": [], 48 | "source": [ 49 | "import numpy as np\n", 50 | "from bqplot import Figure, LinearScale, Axis, ColorScale\n", 51 | "from bqplot_image_gl import ImageGL, Contour\n", 52 | "import ipywidgets as widgets\n", 53 | "scale_x = LinearScale(min=0, max=1)\n", 54 | "scale_y = LinearScale(min=0, max=1)\n", 55 | "scales = {'x': scale_x, 'y': scale_y}\n", 56 | "axis_x = Axis(scale=scale_x, label='x')\n", 57 | "axis_y = Axis(scale=scale_y, label='y', orientation='vertical')\n", 58 | "scales_image = {'x': scale_x, 'y': scale_y, 'image': ColorScale(min=np.min(values).item(), max=np.max(values).item())}\n" 59 | ] 60 | }, 61 | { 62 | "cell_type": "code", 63 | "execution_count": null, 64 | "metadata": {}, 65 | "outputs": [], 66 | "source": [ 67 | "figure = Figure(scales=scales, axes=[axis_x, axis_y])\n", 68 | "image = ImageGL(image=values, scales=scales_image)\n", 69 | "contour = Contour(image=image, level=180, scales=scales_image)\n", 70 | "figure.marks = (image, contour)" 71 | ] 72 | }, 73 | { 74 | "cell_type": "code", 75 | "execution_count": null, 76 | "metadata": {}, 77 | "outputs": [], 78 | "source": [ 79 | "figure" 80 | ] 81 | }, 82 | { 83 | "cell_type": "code", 84 | "execution_count": null, 85 | "metadata": {}, 86 | "outputs": [], 87 | "source": [ 88 | "slider = widgets.FloatSlider(value=contour.level, min=np.min(values).item(), max=np.max(values).item())\n", 89 | "# we link from slider to contour, not back, since we will set multiple levels later on\n", 90 | "widgets.jsdlink((slider, 'value'), (contour, 'level'))\n", 91 | "slider" 92 | ] 93 | }, 94 | { 95 | "cell_type": "code", 96 | "execution_count": null, 97 | "metadata": {}, 98 | "outputs": [], 99 | "source": [ 100 | "cp = widgets.ColorPicker(value='purple')\n", 101 | "widgets.jsdlink((cp, 'value'), (contour, 'color'))\n", 102 | "cp" 103 | ] 104 | }, 105 | { 106 | "cell_type": "code", 107 | "execution_count": null, 108 | "metadata": {}, 109 | "outputs": [], 110 | "source": [ 111 | "contour.color = 'purple'" 112 | ] 113 | }, 114 | { 115 | "cell_type": "markdown", 116 | "metadata": {}, 117 | "source": [ 118 | "# Multiple levels\n", 119 | "If level is a list of values, multiple contours lines will be drawn" 120 | ] 121 | }, 122 | { 123 | "cell_type": "code", 124 | "execution_count": null, 125 | "metadata": {}, 126 | "outputs": [], 127 | "source": [ 128 | "contour.level = 170" 129 | ] 130 | }, 131 | { 132 | "cell_type": "code", 133 | "execution_count": null, 134 | "metadata": {}, 135 | "outputs": [], 136 | "source": [ 137 | "contour.level = [150, 180]" 138 | ] 139 | }, 140 | { 141 | "cell_type": "code", 142 | "execution_count": null, 143 | "metadata": {}, 144 | "outputs": [], 145 | "source": [ 146 | "# but also color\n", 147 | "contour.color = ['red', 'green']" 148 | ] 149 | }, 150 | { 151 | "cell_type": "code", 152 | "execution_count": null, 153 | "metadata": {}, 154 | "outputs": [], 155 | "source": [ 156 | "contour.level = [120, 150, 180]" 157 | ] 158 | }, 159 | { 160 | "cell_type": "code", 161 | "execution_count": null, 162 | "metadata": {}, 163 | "outputs": [], 164 | "source": [ 165 | "contour.level = [120, 180, 150]" 166 | ] 167 | }, 168 | { 169 | "cell_type": "code", 170 | "execution_count": null, 171 | "metadata": {}, 172 | "outputs": [], 173 | "source": [ 174 | "import ipyvuetify as v\n", 175 | "import traitlets\n", 176 | "import ast\n", 177 | "\n", 178 | "class ValueListTextArea(v.TextField):\n", 179 | " values = traitlets.Any()\n", 180 | " \n", 181 | " @traitlets.default('v_model')\n", 182 | " def _v_model(self):\n", 183 | " return \", \".join(map(str, self.values))\n", 184 | "\n", 185 | " @traitlets.default('label')\n", 186 | " def _label(self):\n", 187 | " return \"List of values\"\n", 188 | "\n", 189 | " @traitlets.default('placeholder')\n", 190 | " def _placeholder(self):\n", 191 | " return \"Enter a comma separated list of values\"\n", 192 | "\n", 193 | " @traitlets.default('prepend_icon')\n", 194 | " def _prepend_icon(self):\n", 195 | " return 'show_chart'\n", 196 | "\n", 197 | " @traitlets.observe('v_model')\n", 198 | " def update_custom_selection(self, change):\n", 199 | " self.check_values()\n", 200 | " \n", 201 | " def check_values(self):\n", 202 | " try:\n", 203 | " values = ast.literal_eval(self.v_model)\n", 204 | " except Exception as e:\n", 205 | " self.error_messages = str(e)\n", 206 | " return\n", 207 | " if not isinstance(values, tuple): # maybe we put in a single number\n", 208 | " if not isinstance(values, (int, float)):\n", 209 | " self.error_message = \"Please provide numbers\"\n", 210 | " return\n", 211 | " values = [values]\n", 212 | " for value in values:\n", 213 | " if not isinstance(value, (int, float)):\n", 214 | " self.error_message = \"Please provide numbers\"\n", 215 | " return\n", 216 | " self.error_messages = None\n", 217 | " self.values = values\n", 218 | " return True\n", 219 | "values_list = ValueListTextArea(values=[120])\n", 220 | "values_list" 221 | ] 222 | }, 223 | { 224 | "cell_type": "code", 225 | "execution_count": null, 226 | "metadata": {}, 227 | "outputs": [], 228 | "source": [ 229 | "widgets.dlink((values_list, 'values'), (contour, 'level'))\n", 230 | "figure" 231 | ] 232 | }, 233 | { 234 | "cell_type": "markdown", 235 | "metadata": {}, 236 | "source": [ 237 | "# Kernel side contour lines\n", 238 | "d3 is not the fast way to calculate contour lines. For performance reasons it may be useful to use skimage." 239 | ] 240 | }, 241 | { 242 | "cell_type": "code", 243 | "execution_count": null, 244 | "metadata": {}, 245 | "outputs": [], 246 | "source": [ 247 | "x, y = np.ogrid[-np.pi:np.pi:256j, -np.pi:np.pi:256j]\n", 248 | "values = np.sin(np.exp((np.sin(x)**3 + np.cos(y)**2)))" 249 | ] 250 | }, 251 | { 252 | "cell_type": "code", 253 | "execution_count": null, 254 | "metadata": {}, 255 | "outputs": [], 256 | "source": [ 257 | "contour_level = 0.95" 258 | ] 259 | }, 260 | { 261 | "cell_type": "code", 262 | "execution_count": null, 263 | "metadata": {}, 264 | "outputs": [], 265 | "source": [ 266 | "import skimage.measure\n", 267 | "contours = skimage.measure.find_contours(values.T, contour_level)\n", 268 | "contours = [k/values.T.shape for k in contours]" 269 | ] 270 | }, 271 | { 272 | "cell_type": "code", 273 | "execution_count": null, 274 | "metadata": {}, 275 | "outputs": [], 276 | "source": [ 277 | "import bqplot\n", 278 | "from bqplot import Figure, LinearScale, Axis, ColorScale\n", 279 | "from bqplot_image_gl import ImageGL, Contour\n", 280 | "\n", 281 | "scale_x = LinearScale(min=0, max=1)\n", 282 | "scale_y = LinearScale(min=0, max=1)\n", 283 | "scales = {'x': scale_x, 'y': scale_y}\n", 284 | "axis_x = Axis(scale=scale_x, label='x')\n", 285 | "axis_y = Axis(scale=scale_y, label='y', orientation='vertical')\n", 286 | "scales_image = {'x': scale_x, 'y': scale_y, 'image': ColorScale(min=np.min(values).item(), max=np.max(values).item())}\n", 287 | "\n", 288 | "figure = Figure(scales=scales, axes=[axis_x, axis_y])\n", 289 | "image = ImageGL(image=values, scales=scales_image)\n", 290 | "contour_precomputed = Contour(contour_lines=[contours], level=contour_level, scales=scales_image, label_steps=200)\n", 291 | "figure.marks = (image, contour_precomputed)\n", 292 | "figure" 293 | ] 294 | }, 295 | { 296 | "cell_type": "code", 297 | "execution_count": null, 298 | "metadata": {}, 299 | "outputs": [], 300 | "source": [ 301 | "contour_precomputed.label = ['min', 'max']" 302 | ] 303 | } 304 | ], 305 | "metadata": { 306 | "kernelspec": { 307 | "display_name": "Python 3", 308 | "language": "python", 309 | "name": "python3" 310 | }, 311 | "language_info": { 312 | "codemirror_mode": { 313 | "name": "ipython", 314 | "version": 3 315 | }, 316 | "file_extension": ".py", 317 | "mimetype": "text/x-python", 318 | "name": "python", 319 | "nbconvert_exporter": "python", 320 | "pygments_lexer": "ipython3", 321 | "version": "3.6.7" 322 | } 323 | }, 324 | "nbformat": 4, 325 | "nbformat_minor": 2 326 | } 327 | -------------------------------------------------------------------------------- /js/lib/contour.js: -------------------------------------------------------------------------------- 1 | var version = require('./version').version; 2 | 3 | import * as bqplot from 'bqplot'; 4 | import * as widgets from "@jupyter-widgets/base"; 5 | import * as d3contour from "d3-contour"; 6 | import * as d3geo from "d3-geo"; 7 | import * as d3 from "d3"; 8 | import * as jupyter_dataserializers from "jupyter-dataserializers"; 9 | 10 | 11 | class ContourModel extends bqplot.MarkModel { 12 | 13 | defaults() { 14 | return _.extend(bqplot.MarkModel.prototype.defaults(), { 15 | _model_name : 'ContourModel', 16 | _view_name : 'ContourView', 17 | _model_module : 'bqplot-image-gl', 18 | _view_module : 'bqplot-image-gl', 19 | _model_module_version : version, 20 | _view_module_version : version, 21 | image: null, 22 | level: null, 23 | color: null, 24 | scales_metadata: { 25 | 'x': {'orientation': 'horizontal', 'dimension': 'x'}, 26 | 'y': {'orientation': 'vertical', 'dimension': 'y'}, 27 | }, 28 | }); 29 | } 30 | 31 | initialize(attributes, options) { 32 | super.initialize(attributes, options); 33 | this.on_some_change(['level', 'contour_lines'], this.update_data, this); 34 | this.update_data(); 35 | } 36 | 37 | update_data() { 38 | const image_widget = this.get('image'); 39 | const level = this.get('level') 40 | // we support a single level or multiple 41 | this.thresholds = Array.isArray(level) ? level : [level]; 42 | if(image_widget) { 43 | const image = image_widget.get('image') 44 | this.width = image.shape[1]; 45 | this.height = image.shape[0]; 46 | this.contours = this.thresholds.map((threshold) => d3contour 47 | .contours() 48 | .size([this.width, this.height]) 49 | .contour(image.data, [threshold]) 50 | ) 51 | } else { 52 | this.width = 1; // precomputed contour_lines will have to be in normalized 53 | this.height = 1; // coordinates. 54 | const contour_lines = this.get('contour_lines'); 55 | this.contours = contour_lines.map((contour_line_set) => { 56 | return { 57 | type: 'MultiLineString', 58 | coordinates: contour_line_set.map((contour_line) => { 59 | // this isn't really efficient, if we do real WebGL rendering 60 | // we may keep this as typed array 61 | var values = []; 62 | for(var i = 0; i < contour_line.size/2; i++) { 63 | values.push([contour_line.get(i, 0), contour_line.get(i, 1)]) 64 | } 65 | return values; 66 | }) 67 | } 68 | }) 69 | } 70 | this.trigger("data_updated"); 71 | } 72 | } 73 | 74 | class ContourView extends bqplot.Mark { 75 | create_listeners() { 76 | super.create_listeners(); 77 | this.listenTo(this.model, "change:label_steps change:label", () => { 78 | this.updateLabels() 79 | }) 80 | this.listenTo(this.parent, "margin_updated", () => { 81 | this.set_ranges(); 82 | this.updatePaths(); 83 | this.updateLabels(); 84 | }); 85 | this.listenTo(this.model, "change:color", () => { 86 | // TODO: this is not efficient, but updateColor does not work as it is 87 | // this.updateColors() 88 | this.updatePaths(); 89 | this.updateLabels(); 90 | }); 91 | this.listenTo(this.model, "data_updated", () => { 92 | this.updatePaths() 93 | this.updateLabels() 94 | }); 95 | } 96 | set_positional_scales() { 97 | var x_scale = this.scales.x, 98 | y_scale = this.scales.y; 99 | this.listenTo(x_scale, "domain_changed", function() { 100 | this.updatePaths(); 101 | this.updateLabels(); 102 | }); 103 | this.listenTo(y_scale, "domain_changed", function() { 104 | this.updatePaths(); 105 | this.updateLabels(); 106 | }); 107 | } 108 | updateColors() { 109 | const color = this.getColor(); 110 | this.paths 111 | .data(this.model.thresholds) 112 | .attr("stroke", this.getColor.bind(this)) 113 | this.d3path.attr("stroke", color) 114 | this.d3label_group.selectAll("text").attr("fill", color) 115 | } 116 | getColor(threshold, index) { 117 | let color = this.model.get('color') 118 | if(color) { 119 | const color_array = Array.isArray(color) ? color : [color]; 120 | return color_array[index % color_array.length]; 121 | } 122 | const model = this.model; 123 | var colors = this.scales.image.model.color_range; 124 | var color_scale = d3.scaleLinear() 125 | .range(colors) 126 | .domain(this.scales.image.model.domain); 127 | const min = this.scales.image.model.domain[0]; 128 | const max = this.scales.image.model.domain[this.scales.image.model.domain.length-1]; 129 | const delta = max - min; 130 | // a good default color is one that is 50% off from the value of the colormap 131 | const level_plus_50_percent = ((threshold - min) + delta / 2) % delta + min; 132 | color = color_scale(level_plus_50_percent); 133 | return color; 134 | } 135 | createPath(index) { 136 | const x_scale = this.scales.x, y_scale = this.scales.y; 137 | const model = this.model; 138 | var bqplot_transform = d3geo.geoTransform({ 139 | point: function(x, y) { 140 | // transform x from pixel coordinates to normalized, and then use bqplot's scale 141 | // TODO: we should respect image's x and y 142 | this.stream.point(x_scale.scale(x/model.width), y_scale.scale(y/model.height)); 143 | } 144 | }); 145 | const path = d3geo.geoPath(bqplot_transform)(this.model.contours[index]) 146 | return path; 147 | } 148 | render() { 149 | const promise = super.render() 150 | promise.then(() => { 151 | this.draw() 152 | this.create_listeners(); 153 | 154 | }) 155 | return promise; 156 | } 157 | set_ranges() { 158 | var x_scale = this.scales.x, 159 | y_scale = this.scales.y; 160 | if(x_scale) { 161 | x_scale.set_range(this.parent.padded_range("x", x_scale.model)); 162 | } 163 | if(y_scale) { 164 | y_scale.set_range(this.parent.padded_range("y", y_scale.model)); 165 | } 166 | } 167 | draw() { 168 | // this.mask_id = `${this.cid}-${this.model.cid}` 169 | // this.mask = this.parent.svg.select('defs') 170 | // .append("mask") 171 | // .attr("id", this.mask_id); 172 | this.d3path = this.d3el.append("g");//.attr("stroke", "yellow"); 173 | this.d3label_group = this.d3el.append("g") 174 | this.updatePaths() 175 | this.updateLabels() 176 | } 177 | updatePaths() { 178 | this.paths = this.d3el.select("g").selectAll("path").data(this.model.thresholds); 179 | const enter = this.paths.enter().append("path"); 180 | // we set attrs on the new and existing elements (hence the merge) 181 | this.paths.merge(enter) 182 | .attr("stroke", this.getColor.bind(this)) 183 | .attr("fill", "none") 184 | .attr("d", (threshold, index) => { 185 | return this.createPath(index) 186 | }) 187 | .attr("mask", this.mask_id) 188 | ; 189 | this.paths.exit().remove(); 190 | 191 | } 192 | updateLabels() { 193 | const x_scale = this.scales.x, y_scale = this.scales.y; 194 | const model = this.model; 195 | 196 | this.d3label_group.html(null) // removes all children 197 | const margin = this.parent.margin; 198 | 199 | 200 | this.model.contours.forEach((contour, index) => { 201 | const color = this.getColor(model.thresholds[index], index); 202 | let label = this.model.get('label') 203 | if(label) { 204 | const label_array = Array.isArray(label) ? label : [label]; 205 | label = label_array[index % label_array.length]; 206 | } else { 207 | label = String(model.thresholds[index]); 208 | } 209 | // http://wiki.geojson.org/GeoJSON_draft_version_6#MultiPolygon 210 | const is_polygon = contour.type == 'MultiPolygon'; 211 | contour.coordinates.forEach(polygon => { 212 | // a MultiPolygon is a list of rings 213 | // http://wiki.geojson.org/GeoJSON_draft_version_6#Polygon 214 | const linestring_list = is_polygon ? polygon : [polygon] 215 | linestring_list.forEach((line_list, j) => { 216 | // in the case of multipolygons, the beginning and end are the same. 217 | const points = is_polygon ? line_list.slice(1) : line_list; 218 | var index = 0; 219 | const step = this.model.get('label_steps'); 220 | // transform image pixel to bqplot/svg pixel coordinates 221 | const scalex = (_) => x_scale.scale(_/model.width) 222 | const scaley = (_) => y_scale.scale(_/model.height) 223 | while(index < points.length) { 224 | const index_previous = (index - 1 + points.length) % points.length; 225 | const index_next = (index + 1 + points.length) % points.length; 226 | const x_current = scalex(points[index][0]) 227 | const y_current = scaley(points[index][1]) 228 | const x_previous = scalex(points[index_previous][0]); 229 | const y_previous = scaley(points[index_previous][1]); 230 | const x_next = scalex(points[index_next][0]); 231 | const y_next = scaley(points[index_next][1]); 232 | const dx = x_next - x_previous; 233 | const dy = y_next - y_previous; 234 | var label_angle = (Math.atan2(dy, dx) * 180 / Math.PI + 180) % 360; 235 | // if the label is upside down, we wanna rotate an extra 180 degrees 236 | if(label_angle > 270) 237 | label_angle = (label_angle + 180) % 360; 238 | if(label_angle > 90) 239 | label_angle = (label_angle + 180) % 360; 240 | this.d3label_group 241 | .append("text") 242 | .text(label) 243 | .attr("transform", `translate(${x_current}, ${y_current}) rotate(${label_angle})`) 244 | .attr("text-anchor", "middle") 245 | .attr("fill", color) 246 | // this.mask 247 | // .append("circle") 248 | // .attr("r", 20) 249 | // .attr("fill", "black") 250 | // .attr("transform", `translate(${x_current}, ${y_current})`); 251 | index += step; 252 | // we don't want do draw close to the end 253 | if(index > (points.length - step*1.2)) 254 | break; 255 | } 256 | }) 257 | }) 258 | }) 259 | } 260 | } 261 | 262 | ContourModel.serializers = Object.assign({}, bqplot.MarkModel.serializers, { 263 | image: {deserialize: widgets.unpack_models}, 264 | contour_lines: {deserialize: (obj, manager) => { 265 | return obj.map((countour_line_set) => countour_line_set.map((contour_line) => { 266 | let state = {buffer: contour_line.value, dtype: contour_line.dtype, shape: contour_line.shape}; 267 | return jupyter_dataserializers.JSONToArray(state); 268 | 269 | })); 270 | }} 271 | }); 272 | 273 | export { 274 | ContourModel, ContourView 275 | } 276 | -------------------------------------------------------------------------------- /js/lib/values.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // NOTE: this is the generate js file from ipyvolume/js/values.ts 3 | var __importStar = (this && this.__importStar) || function (mod) { 4 | if (mod && mod.__esModule) return mod; 5 | var result = {}; 6 | if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; 7 | result["default"] = mod; 8 | return result; 9 | }; 10 | Object.defineProperty(exports, "__esModule", { value: true }); 11 | // var _ = require("underscore"); 12 | const lodash_1 = require("lodash"); 13 | const THREE = __importStar(require("three")); 14 | const utils = __importStar(require("./utils")); 15 | /* Manages a list of scalar and arrays for use with WebGL instanced rendering 16 | */ 17 | class Values { 18 | constructor(names, names_vec3, getter, sequence_index, names_vec4 = []) { 19 | this.length = Infinity; 20 | this.scalar = {}; 21 | this.scalar_vec3 = {}; 22 | this.scalar_vec4 = {}; 23 | this.array = {}; 24 | this.array_vec3 = {}; 25 | this.array_vec4 = {}; 26 | this.values = {}; 27 | // _.each(names, (name) => { 28 | for (const name of names) { 29 | const value = getter(name, sequence_index, Values.defaults[name]); 30 | if (utils.is_typedarray(value)) { 31 | if (name !== "selected") { // hardcoded.. hmm bad 32 | this.length = Math.min(this.length, value.length); 33 | } 34 | this.array[name] = value; 35 | } 36 | else { 37 | this.scalar[name] = value; 38 | } 39 | this.values[name] = value; 40 | } 41 | for (const name of names_vec3) { 42 | let value = getter(name, sequence_index, Values.defaults[name]); 43 | if (name.indexOf("color") !== -1 && typeof value === "string") { 44 | // special case to support controlling color from a widget 45 | const color = new THREE.Color(value); 46 | // no sequence, scalar 47 | value = new Float32Array([color.r, color.g, color.b]); 48 | } 49 | if (utils.is_typedarray(value) && value.length > 3) { 50 | // single value is interpreted as scalar 51 | this.array_vec3[name] = value; 52 | this.length = Math.min(this.length, value.length / 3); 53 | } 54 | else { 55 | this.scalar_vec3[name] = value; 56 | } 57 | this.values[name] = value; 58 | } 59 | for (const name of names_vec4) { 60 | let value = getter(name, sequence_index, Values.defaults[name]); 61 | if (name.indexOf("color") !== -1 && typeof value === "string") { 62 | // special case to support controlling color from a widget 63 | const color = new THREE.Color(value); 64 | value = new Float32Array([color.r, color.g, color.b, 1.0]); 65 | } 66 | if (utils.is_typedarray(value) && value.length > 4) { 67 | this.array_vec4[name] = value; 68 | // color vectors have 4 components 69 | this.length = Math.min(this.length, value.length / 4); 70 | } 71 | else { 72 | // single value is interpreted as scalar 73 | this.scalar_vec4[name] = value; 74 | } 75 | this.values[name] = value; 76 | } 77 | } 78 | trim(new_length) { 79 | this.array = lodash_1.mapValues(this.array, (array) => { 80 | return array.length === new_length ? array : array.slice(0, new_length); 81 | }); 82 | this.array_vec3 = lodash_1.mapValues(this.array_vec3, (array_vec3) => { 83 | return array_vec3.length === new_length * 3 ? array_vec3 : array_vec3.slice(0, new_length * 3); 84 | }); 85 | this.array_vec4 = lodash_1.mapValues(this.array_vec4, (array_vec4) => { 86 | return (array_vec4.length === new_length * 4) ? array_vec4 : array_vec4.slice(0, new_length * 4); 87 | }); 88 | this.length = new_length; 89 | } 90 | ensure_array(name_or_names) { 91 | const names = lodash_1.isArray(name_or_names) ? name_or_names : [name_or_names]; 92 | for (const name of names) { 93 | if (typeof this.scalar[name] !== "undefined") { 94 | const array = this.array[name] = new Float32Array(this.length); 95 | array.fill(this.scalar[name]); 96 | delete this.scalar[name]; 97 | delete this.values[name]; 98 | } 99 | const value_vec3 = this.scalar_vec3[name]; 100 | const value_vec4 = this.scalar_vec4[name]; 101 | if (typeof value_vec3 !== "undefined") { 102 | const array = this.array_vec3[name] = new Float32Array(this.length * 3); 103 | for (let i = 0; i < this.length; i++) { 104 | array[i * 3 + 0] = value_vec3[0]; 105 | array[i * 3 + 1] = value_vec3[1]; 106 | array[i * 3 + 2] = value_vec3[2]; 107 | } 108 | delete this.scalar_vec3[name]; 109 | delete this.values[name]; 110 | } 111 | if (typeof value_vec4 !== "undefined") { 112 | this.array_vec4[name] = new Float32Array(this.length * 4); 113 | const array = this.array_vec4[name]; 114 | for (let i = 0; i < this.length; i++) { 115 | array[i * 4 + 0] = value_vec4[0]; 116 | array[i * 4 + 1] = value_vec4[1]; 117 | array[i * 4 + 2] = value_vec4[2]; 118 | array[i * 4 + 3] = value_vec4[3]; 119 | } 120 | delete this.scalar_vec4[name]; 121 | delete this.values[name]; 122 | } 123 | } 124 | } 125 | grow(new_length) { 126 | this.array = lodash_1.mapValues(this.array, (array) => { 127 | const new_array = new array.constructor(new_length); 128 | new_array.set(array); 129 | return new_array; 130 | }); 131 | this.array_vec3 = lodash_1.mapValues(this.array_vec3, (array_vec3) => { 132 | const new_array = new array_vec3.constructor(new_length * 3); 133 | new_array.set(array_vec3); 134 | return new_array; 135 | }); 136 | this.length = length; 137 | } 138 | pad(other) { 139 | this.array = lodash_1.mapValues(this.array, (array, name) => { 140 | const new_array = new array.constructor(other.length); 141 | if (typeof other.array[name] === "undefined") { // then other must be a scalar 142 | new_array.fill(other.scalar[name], this.length); 143 | } 144 | else { 145 | new_array.set(other.array[name].slice(this.length), this.length); 146 | } 147 | new_array.set(array); 148 | return new_array; 149 | }); 150 | this.array_vec3 = lodash_1.mapValues(this.array_vec3, (array_vec3, name) => { 151 | const new_array = new array_vec3.constructor(other.length * 3); 152 | if (typeof other.array_vec3[name] === "undefined") { // then other must be a scalar 153 | const other_scalar = other.scalar_vec3[name]; 154 | for (let i = this.length; i < other.length; i++) { 155 | new_array[i * 3 + 0] = other_scalar[0]; 156 | new_array[i * 3 + 1] = other_scalar[1]; 157 | new_array[i * 3 + 2] = other_scalar[2]; 158 | } 159 | } 160 | else { 161 | new_array.set(other.array_vec3[name].slice(this.length * 3), this.length * 3); 162 | } 163 | new_array.set(array_vec3); 164 | return new_array; 165 | }); 166 | this.array_vec4 = lodash_1.mapValues(this.array_vec4, (array_vec4, name) => { 167 | const new_array = new array_vec4.constructor(other.length * 4); 168 | if (typeof other.array_vec4[name] === "undefined") { 169 | // then other must be a scalar 170 | const other_scalar = other.scalar_vec4[name]; 171 | for (let i = this.length; i < other.length; i++) { 172 | new_array[i * 4 + 0] = other_scalar[0]; 173 | new_array[i * 4 + 1] = other_scalar[1]; 174 | new_array[i * 4 + 2] = other_scalar[2]; 175 | new_array[i * 4 + 3] = other_scalar[3]; 176 | } 177 | } 178 | else { 179 | new_array.set(other.array_vec4[name].slice(this.length * 4), this.length * 4); 180 | } 181 | new_array.set(array_vec4); 182 | return new_array; 183 | }); 184 | this.length = other.length; 185 | } 186 | select(selected) { 187 | // copy since we will modify 188 | const sizes = this.array.size = this.array.size.slice(); 189 | const size_selected = this.array.size_selected; 190 | // copy since we will modify 191 | const color = this.array_vec4.color = this.array_vec4.color.slice(); 192 | const color_selected = this.array_vec4.color_selected; 193 | // this assumes, and requires that color_selected is an array, maybe a bit inefficient 194 | selected.forEach((element, index) => { 195 | if (index < this.length) { 196 | sizes[index] = size_selected[index]; 197 | color[index * 4 + 0] = color_selected[index * 4 + 0]; 198 | color[index * 4 + 1] = color_selected[index * 4 + 1]; 199 | color[index * 4 + 2] = color_selected[index * 4 + 2]; 200 | color[index * 4 + 3] = color_selected[index * 4 + 3]; 201 | } 202 | }); 203 | } 204 | merge_to_vec3(names, new_name) { 205 | const element_length = names.length; 206 | const array = new Float32Array(this.length * element_length); // Float32Array should be replaced by a good common value 207 | names.forEach((name, index) => { 208 | this.ensure_array(name); 209 | const array1d = this.array[name]; 210 | for (let i = 0; i < this.length; i++) { 211 | array[i * element_length + index] = array1d[i]; 212 | } 213 | delete this.array[name]; 214 | delete this.values[name]; 215 | }); 216 | this.array_vec3[new_name] = array; 217 | } 218 | pop(name_or_names) { 219 | const names = lodash_1.isArray(name_or_names) ? name_or_names : [name_or_names]; 220 | // _.each(names, (name) => { 221 | names.forEach((name, index) => { 222 | [this.scalar, 223 | this.scalar_vec3, 224 | this.array, 225 | this.array_vec3].forEach((storage) => { 226 | if (typeof storage[name] !== "undefined") { 227 | delete storage[name]; 228 | } 229 | }); 230 | }); 231 | } 232 | add_attributes(geometry, postfix = "") { 233 | postfix = postfix; 234 | // set all attributes 235 | lodash_1.forOwn(this.array, (array, name) => { 236 | if (name.indexOf("selected") === -1) { 237 | // selected attributes should not be send to the shader 238 | const attr = new THREE.InstancedBufferAttribute(array, 1, false, 1); 239 | geometry.addAttribute(name + postfix, attr); 240 | } 241 | }); 242 | lodash_1.forOwn(this.array_vec3, (array, name) => { 243 | if (name.indexOf("selected") === -1) { 244 | // selected attributes should not be send to the shader 245 | const attr = new THREE.InstancedBufferAttribute(array, 3, false, 1); 246 | attr.normalized = name.indexOf("color") === -1 ? false : true; // color should be normalized 247 | geometry.addAttribute(name + postfix, attr); 248 | } 249 | }); 250 | lodash_1.forOwn(this.array_vec4, (array, name) => { 251 | if (name.indexOf("selected") === -1) { 252 | // selected attributes should not be send to the shader 253 | const attr = new THREE.InstancedBufferAttribute(array, 4, false, 1); 254 | attr.normalized = name.indexOf("color") === -1 ? false : true; // color should be normalized 255 | geometry.addAttribute(name + postfix, attr); 256 | } 257 | }); 258 | lodash_1.forOwn(this.scalar, (scalar, name) => { 259 | if (name.indexOf("selected") === -1) { 260 | // selected attributes should not be send to the shader 261 | const attr = new THREE.InstancedBufferAttribute(new Float32Array([scalar]), 1, false, this.length); 262 | geometry.addAttribute(name + postfix, attr); 263 | } 264 | }); 265 | lodash_1.forOwn(this.scalar_vec3, (scalar_vec3, name) => { 266 | if (name.indexOf("selected") === -1) { 267 | // selected attributes should not be send to the shader 268 | const attr = new THREE.InstancedBufferAttribute(scalar_vec3, 3, false, this.length); 269 | attr.normalized = name.indexOf("color") === -1 ? false : true; // color should be normalized 270 | geometry.addAttribute(name + postfix, attr); 271 | } 272 | }); 273 | lodash_1.forOwn(this.scalar_vec4, (scalar_vec4, name) => { 274 | if (name.indexOf("selected") === -1) { 275 | // selected attributes should not be send to the shader 276 | const attr = new THREE.InstancedBufferAttribute(scalar_vec4, 4, false, this.length); 277 | attr.normalized = name.indexOf("color") === -1 ? false : true; // color should be normalized 278 | geometry.addAttribute(name + postfix, attr); 279 | } 280 | }); 281 | } 282 | } 283 | exports.Values = Values; 284 | Values.defaults = { 285 | vx: 0, 286 | vy: 1, 287 | vz: 0, 288 | x: 0, 289 | y: 0, 290 | z: 0, 291 | size: 0, 292 | }; 293 | -------------------------------------------------------------------------------- /js/lib/BrushEllipseSelector.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importStar = (this && this.__importStar) || function (mod) { 3 | if (mod && mod.__esModule) return mod; 4 | var result = {}; 5 | if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; 6 | result["default"] = mod; 7 | return result; 8 | }; 9 | Object.defineProperty(exports, "__esModule", { value: true }); 10 | const BaseXYSelector = __importStar(require("bqplot")).BaseXYSelector; 11 | const d3 = require("d3"); 12 | const d3SelMulti = require("d3-selection-multi"); 13 | const d3_drag_1 = require("d3-drag"); 14 | const d3Selection = require("d3-selection"); 15 | const { applyStyles } = require("./utils"); 16 | const d3GetEvent = function () { return require("d3-selection").event; }.bind(this); 17 | /* 18 | good resource: https://en.wikipedia.org/wiki/Ellipse 19 | Throughout we use rx==a and ry==b, assuming like the article above: 20 | x**2/a**2 + y**2/b**2==1 21 | 22 | Useful equations: 23 | y = +/- b/a * sqrt(a**2 -x**2) 24 | a = sqrt(y**2 a**2/b**2 + x**2) 25 | x = a cos(t) 26 | y = b sin(t) (where t is called the eccentric anomaly or just 'angle) 27 | y/x = b/a sin(t)/cos(t) = b/a tan(t) 28 | a y/ b x = a/b cos(t)/sin(t) = tan(t) 29 | t = atan2(a y, b x) 30 | */ 31 | class BrushEllipseSelector extends BaseXYSelector { 32 | constructor() { 33 | super(...arguments); 34 | // for testing purposes we need to keep track of this at the instance level 35 | this.brushStartPosition = { x: 0, y: 0 }; 36 | this.moveStartPosition = { x: 0, y: 0 }; 37 | this.reshapeStartAngle = 0; 38 | this.reshapeStartRadii = { rx: 0, ry: 0 }; 39 | } 40 | async render() { 41 | super.render(); 42 | 43 | const scale_creation_promise = this.create_scales(); 44 | await Promise.all([this.mark_views_promise, scale_creation_promise]); 45 | this.create_listeners(); 46 | // we need to create our copy of this d3 selection, since we got out own copy of d3 47 | const d3el = d3.select(this.d3el.node()); 48 | d3el.attr('clip-path', "url(#" + this.parent.clip_id + ")") 49 | this.eventElement = d3el.append('rect') 50 | .attr("x", 0) 51 | .attr("y", 0) 52 | .attr("width", this.width) 53 | .attr("height", this.height) 54 | .attr("pointer-events", "all") 55 | .style("cursor", "crosshair") 56 | .style("visibility", "hidden") 57 | ; 58 | d3el.attr("class", "selector brushintsel") 59 | this.brush = d3el.append('g') 60 | .style("visibility", "visible"); 61 | 62 | this.d3ellipseHandle = this.brush.append("ellipse"); 63 | this.d3ellipse = this.brush.append("ellipse"); 64 | this.eventElement.call(d3_drag_1.drag().on("start", () => { 65 | const e = d3GetEvent(); 66 | this._brushStart({ x: e.x, y: e.y }); 67 | }).on("drag", () => { 68 | const e = d3GetEvent(); 69 | this._brushDrag({ x: e.x, y: e.y }); 70 | }).on("end", () => { 71 | const e = d3GetEvent(); 72 | this._brushEnd({ x: e.x, y: e.y }); 73 | })); 74 | // events for moving the existing ellipse 75 | this.d3ellipse.call(d3_drag_1.drag().on("start", () => { 76 | const e = d3GetEvent(); 77 | this._moveStart({ x: e.x, y: e.y }); 78 | }).on("drag", () => { 79 | const e = d3GetEvent(); 80 | this._moveDrag({ x: e.x, y: e.y }); 81 | }).on("end", () => { 82 | const e = d3GetEvent(); 83 | this._moveEnd({ x: e.x, y: e.y }); 84 | })); 85 | // events for reshaping the existing ellipse 86 | this.d3ellipseHandle.call(d3_drag_1.drag().on("start", () => { 87 | const e = d3GetEvent(); 88 | this._reshapeStart({ x: e.x, y: e.y }); 89 | }).on("drag", () => { 90 | const e = d3GetEvent(); 91 | this._reshapeDrag({ x: e.x, y: e.y }); 92 | }).on("end", () => { 93 | const e = d3GetEvent(); 94 | this._reshapeEnd({ x: e.x, y: e.y }); 95 | })); 96 | this.updateEllipse(); 97 | this.syncSelectionToMarks(); 98 | this.listenTo(this.model, 'change:selected_x change:selected_y change:color change:style change:border_style', () => this.updateEllipse()); 99 | this.listenTo(this.model, 'change:selected_x change:selected_y', this.syncSelectionToMarks); 100 | } 101 | relayout() { 102 | super.relayout(); 103 | this.x_scale.set_range(this.parent.padded_range("x", this.x_scale.model)); 104 | this.y_scale.set_range(this.parent.padded_range("y", this.y_scale.model)); 105 | // Called when the figure margins are updated. 106 | this.eventElement 107 | .attr("width", this.parent.width - 108 | this.parent.margin.left - 109 | this.parent.margin.right) 110 | .attr("height", this.parent.height - 111 | this.parent.margin.top - 112 | this.parent.margin.bottom); 113 | this.updateEllipse() 114 | } 115 | remove() { 116 | super.remove() 117 | // detach the event listener for dragging, since they are attached to the parent 118 | const bg_events = d3.select(this.parent.bg_events.node()) 119 | bg_events.on(".start .drag .end", null); 120 | } 121 | // these methods are not private, but are used for testing, they should not be used as a public API. 122 | _brushStart({ x, y }) { 123 | console.log('start', x, y); 124 | this.brushStartPosition = { x, y }; 125 | this.model.set("brushing", true); 126 | this.touch(); 127 | } 128 | _brushDrag({ x, y }) { 129 | console.log('drag', x, y); 130 | this._brush({ x, y }); 131 | } 132 | _brushEnd({ x, y }) { 133 | console.log('end', x, y); 134 | this._brush({ x, y }); 135 | this.model.set("brushing", false); 136 | this.touch(); 137 | } 138 | _brush({ x, y }) { 139 | const cx = this.brushStartPosition.x; 140 | const cy = this.brushStartPosition.y; 141 | const relX = Math.abs(x - cx); 142 | const relY = Math.abs(y - cy); 143 | if (!this.model.get('pixel_aspect') && ((relX == 0) || (relY == 0))) { 144 | console.log('cannot draw ellipse'); 145 | this.model.set('selected_x', null); 146 | this.model.set('selected_y', null); 147 | this.touch(); 148 | return; // we can't draw an ellipse or circle 149 | } 150 | // if 'feels' natural to have a/b == relX/relY, meaning the aspect ratio of the ellipse equals that of the pixels moved 151 | // but the aspect can be overridden by the model, to draw for instance circles 152 | let ratio = this.model.get('pixel_aspect') || (relX / relY); 153 | // using ra = a = sqrt(y**2 a**2/b**2 + x**2) we can solve a, from x, y, and the ratio a/b 154 | const rx = Math.sqrt(relY * relY * ratio * ratio + relX * relX); 155 | // and from that solve ry == b 156 | const ry = rx / ratio; 157 | // bounding box of the ellipse in pixel coordinates: 158 | const [px1, px2, py1, py2] = [cx - rx, cx + rx, cy - ry, cy + ry]; 159 | // we don't want a single click to trigger an empty selection 160 | if (!((px1 == px2) && (py1 == py2))) { 161 | let selectedX = [px1, px2].map((pixel) => this.x_scale.scale.invert(pixel)); 162 | let selectedY = [py1, py2].map((pixel) => this.y_scale.scale.invert(pixel)); 163 | this.model.set('selected_x', new Float32Array(selectedX)); 164 | this.model.set('selected_y', new Float32Array(selectedY)); 165 | this.touch(); 166 | } 167 | else { 168 | this.model.set('selected_x', null); 169 | this.model.set('selected_y', null); 170 | this.touch(); 171 | } 172 | } 173 | _moveStart({ x, y }) { 174 | this.moveStartPosition = { x, y }; 175 | this.model.set("brushing", true); 176 | this.touch(); 177 | } 178 | _moveDrag({ x, y }) { 179 | this._move({ dx: x - this.moveStartPosition.x, dy: y - this.moveStartPosition.y }); 180 | this.moveStartPosition = { x, y }; 181 | } 182 | _moveEnd({ x, y }) { 183 | this._move({ dx: x - this.moveStartPosition.x, dy: y - this.moveStartPosition.y }); 184 | this.model.set("brushing", false); 185 | this.touch(); 186 | } 187 | _move({ dx, dy }) { 188 | // move is in pixels, so we need to transform to the domain 189 | const { px1, px2, py1, py2 } = this.calculatePixelCoordinates(); 190 | let selectedX = [px1, px2].map((pixel) => this.x_scale.scale.invert(pixel + dx)); 191 | let selectedY = [py1, py2].map((pixel) => this.y_scale.scale.invert(pixel + dy)); 192 | this.model.set('selected_x', new Float32Array(selectedX)); 193 | this.model.set('selected_y', new Float32Array(selectedY)); 194 | this.touch(); 195 | } 196 | _reshapeStart({ x, y }) { 197 | const { cx, cy, rx, ry } = this.calculatePixelCoordinates(); 198 | const ratio = this.model.get('pixel_aspect'); 199 | if (ratio) { 200 | // reshaping with an aspect ratio is done equivalent to starting a new brush on the current ellipse coordinate 201 | this._brushStart({ x: cx, y: cy }); 202 | this._brushDrag({ x, y }); 203 | } 204 | else { 205 | const relX = x - cx; 206 | const relY = y - cy; 207 | // otherwise, we deform the ellipse by 'dragging' the ellipse at the angle we grab it 208 | this.reshapeStartAngle = Math.atan2(rx * relY, ry * relX); 209 | this.reshapeStartRadii = { rx, ry }; 210 | } 211 | this.model.set("brushing", true); 212 | this.touch(); 213 | } 214 | _reshapeDrag({ x, y }) { 215 | const ratio = this.model.get('pixel_aspect'); 216 | if (ratio) { 217 | this._brushDrag({ x, y }); 218 | } 219 | else { 220 | this._reshape({ x: x, y: y, angle: this.reshapeStartAngle }); 221 | } 222 | } 223 | _reshapeEnd({ x, y }) { 224 | const ratio = this.model.get('pixel_aspect'); 225 | if (ratio) { 226 | this._brushEnd({ x, y }); 227 | } 228 | else { 229 | this._reshape({ x: x, y: y, angle: this.reshapeStartAngle }); 230 | } 231 | this.model.set("brushing", false); 232 | this.touch(); 233 | } 234 | _reshape({ x, y, angle }) { 235 | const { cx, cy } = this.calculatePixelCoordinates(); 236 | // if we are within -10,+10 degrees within 0, 90, 180, 270, or 360 degrees 237 | // 'round' to that angle 238 | angle = (angle + Math.PI * 2) % (Math.PI * 2); 239 | for (let i = 0; i < 5; i++) { 240 | const angleTest = Math.PI * i / 2; 241 | const angle1 = angleTest - 10 * Math.PI / 180; 242 | const angle2 = angleTest + 10 * Math.PI / 180; 243 | console.log('test angle', angleTest, angle1, angle2, ((angle > angle1) && (angle < angle2))); 244 | if ((angle > angle1) && (angle < angle2)) { 245 | angle = angleTest; 246 | } 247 | } 248 | angle = (angle + Math.PI * 2) % (Math.PI * 2); 249 | const relX = (x - cx); 250 | const relY = (y - cy); 251 | /* 252 | Solve, for known t=angle 253 | relX = rx cos(t) 254 | relY = ry sin(t) (where t is called the eccentric anomaly or just 'angle) 255 | */ 256 | let ratio = this.model.get('pixel_aspect'); 257 | let rx = relX / (Math.cos(angle)); 258 | let ry = relY / (Math.sin(angle)); 259 | // if we are at one of the 4 corners, we fix rx, ry, or scaled by the ratio 260 | if ((angle == Math.PI / 2) || (angle == Math.PI * 3 / 2)) { 261 | if (ratio) { 262 | rx = ry / ratio; 263 | } 264 | else { 265 | rx = this.reshapeStartRadii.rx; 266 | } 267 | } 268 | if ((angle == 0) || (angle == Math.PI)) { 269 | if (ratio) { 270 | ry = rx * ratio; 271 | } 272 | else { 273 | ry = this.reshapeStartRadii.ry; 274 | } 275 | } 276 | // // bounding box of the ellipse in pixel coordinates: 277 | const [px1, px2, py1, py2] = [cx - rx, cx + rx, cy - ry, cy + ry]; 278 | let selectedX = [px1, px2].map((pixel) => this.x_scale.scale.invert(pixel)); 279 | let selectedY = [py1, py2].map((pixel) => this.y_scale.scale.invert(pixel)); 280 | this.model.set('selected_x', new Float32Array(selectedX)); 281 | this.model.set('selected_y', new Float32Array(selectedY)); 282 | this.touch(); 283 | } 284 | reset() { 285 | this.model.set('selected_x', null); 286 | this.model.set('selected_y', null); 287 | this.touch(); 288 | } 289 | selected_changed() { 290 | // I don't think this should be an abstract method we should implement 291 | // would be good to refactor the interact part a bit 292 | } 293 | canDraw() { 294 | const selectedX = this.model.get('selected_x'); 295 | const selectedY = this.model.get('selected_y'); 296 | return Boolean(selectedX) && Boolean(selectedY); 297 | } 298 | calculatePixelCoordinates() { 299 | if (!this.canDraw()) { 300 | throw new Error("No selection present"); 301 | } 302 | const selectedX = this.model.get('selected_x'); 303 | const selectedY = this.model.get('selected_y'); 304 | var sortFunction = (a, b) => a - b; 305 | let x = [...selectedX].sort(sortFunction); 306 | let y = [...selectedY].sort(sortFunction); 307 | // convert to pixel coordinates 308 | let [px1, px2] = x.map((v) => this.x_scale.scale(v)); 309 | let [py1, py2] = y.map((v) => this.y_scale.scale(v)); 310 | // bounding box, and svg coordinates 311 | return { px1, px2, py1, py2, cx: (px1 + px2) / 2, cy: (py1 + py2) / 2, rx: Math.abs(px2 - px1) / 2, ry: Math.abs(py2 - py1) / 2 }; 312 | } 313 | updateEllipse(offsetX = 0, offsetY = 0, extraRx = 0, extraRy = 0) { 314 | if (!this.canDraw()) { 315 | this.brush.node().style.display = 'none'; 316 | } 317 | else { 318 | const { cx, cy, rx, ry } = this.calculatePixelCoordinates(); 319 | this.d3ellipse 320 | .attr("cx", cx + offsetX) 321 | .attr("cy", cy + offsetY) 322 | .attr("rx", rx + extraRx) 323 | .attr("ry", ry + extraRy) 324 | .style('fill', this.model.get('color') || 'grey'); 325 | applyStyles(this.d3ellipse, this.model.get('style')); 326 | this.d3ellipseHandle 327 | .attr("cx", cx + offsetX) 328 | .attr("cy", cy + offsetY) 329 | .attr("rx", rx + extraRx) 330 | .attr("ry", ry + extraRy) 331 | .style('stroke', this.model.get('color') || 'black') 332 | applyStyles(this.d3ellipseHandle, this.model.get('border_style')); 333 | this.brush.node().style.display = ''; 334 | } 335 | } 336 | syncSelectionToMarks() { 337 | if (!this.canDraw()) 338 | return; 339 | const { cx, cy, rx, ry } = this.calculatePixelCoordinates(); 340 | const point_selector = function (p) { 341 | const [pointX, pointY] = p; 342 | const dx = (cx - pointX) / rx; 343 | const dy = (cy - pointY) / ry; 344 | const insideCircle = (dx * dx + dy * dy) <= 1; 345 | return insideCircle; 346 | }; 347 | const rect_selector = function (xy) { 348 | // TODO: Leaving this to someone who has a clear idea on how this should be implemented 349 | // and who needs it. I don't see a good use case for this (Maarten Breddels). 350 | console.error('Rectangle selector not implemented'); 351 | return false; 352 | }; 353 | this.mark_views.forEach((markView) => { 354 | markView.selector_changed(point_selector, rect_selector); 355 | }); 356 | } 357 | } 358 | exports.BrushEllipseSelector = BrushEllipseSelector; 359 | -------------------------------------------------------------------------------- /js/lib/imagegl.js: -------------------------------------------------------------------------------- 1 | /*jshint esversion: 6 */ 2 | 3 | var version = require('./version').version; 4 | 5 | var widgets = require('@jupyter-widgets/base'); 6 | var _ = require('lodash'); 7 | var d3 = require("d3"); 8 | var bqplot = require('bqplot'); 9 | var THREE = require('three'); 10 | 11 | var interpolations = {'nearest': THREE.NearestFilter, 'bilinear': THREE.LinearFilter}; 12 | 13 | var jupyter_dataserializers = require("jupyter-dataserializers"); 14 | var serialize = require("./serialize"); 15 | 16 | class ImageGLModel extends bqplot.MarkModel { 17 | 18 | defaults() { 19 | return _.extend(bqplot.MarkModel.prototype.defaults(), { 20 | _model_name : 'ImageGLModel', 21 | _view_name : 'ImageGLView', 22 | _model_module : 'bqplot-image-gl', 23 | _view_module : 'bqplot-image-gl', 24 | _model_module_version : version, 25 | _view_module_version : version, 26 | interpolation: 'nearest', 27 | opacity: 1.0, 28 | x: (0.0, 1.0), 29 | y: (0.0, 1.0), 30 | scales_metadata: { 31 | 'x': {'orientation': 'horizontal', 'dimension': 'x'}, 32 | 'y': {'orientation': 'vertical', 'dimension': 'y'}, 33 | }, 34 | }); 35 | } 36 | 37 | initialize(attributes, options) { 38 | super.initialize(attributes, options); 39 | this.on_some_change(['x', 'y'], this.update_data, this); 40 | this.on_some_change(["preserve_domain"], this.update_domains, this); 41 | this.update_data(); 42 | } 43 | 44 | update_data() { 45 | this.mark_data = { 46 | x: this.get("x"), y: this.get("y") 47 | }; 48 | this.update_domains(); 49 | this.trigger("data_updated"); 50 | } 51 | 52 | update_domains() { 53 | if(!this.mark_data) { 54 | return; 55 | } 56 | var scales = this.get("scales"); 57 | var x_scale = scales.x; 58 | var y_scale = scales.y; 59 | 60 | if(x_scale) { 61 | if(!this.get("preserve_domain").x) { 62 | x_scale.compute_and_set_domain(this.mark_data.x, this.model_id + "_x"); 63 | } else { 64 | x_scale.del_domain([], this.model_id + "_x"); 65 | } 66 | } 67 | if(y_scale) { 68 | if(!this.get("preserve_domain").y) { 69 | y_scale.compute_and_set_domain(this.mark_data.y, this.model_id + "_y"); 70 | } else { 71 | y_scale.del_domain([], this.model_id + "_y"); 72 | } 73 | } 74 | } 75 | 76 | } 77 | 78 | ImageGLModel.serializers = Object.assign({}, bqplot.MarkModel.serializers, 79 | { x: serialize.array_or_json, 80 | y: serialize.array_or_json, 81 | image: { 82 | deserialize: (obj, manager) => { 83 | let state = {buffer: obj.value, dtype: obj.dtype, shape: obj.shape}; 84 | return jupyter_dataserializers.JSONToArray(state); 85 | }, 86 | serialize: (ar) => { 87 | const {buffer, dtype, shape} = jupyter_dataserializers.arrayToJSON(ar); 88 | return {value: buffer, dtype:dtype, shape:shape} 89 | } 90 | }}); 91 | 92 | class ImageGLView extends bqplot.Mark { 93 | 94 | render() { 95 | var base_render_promise = super.render(); 96 | window.last_image = this; 97 | 98 | this.image_plane = new THREE.PlaneBufferGeometry( 1.0, 1.0 ); 99 | this.image_material = new THREE.ShaderMaterial( { 100 | uniforms: { 101 | image: { type: 't', value: null }, 102 | // the domain of the image pixel data (for intensity only, for rgb this is ignored) 103 | // these 3 uniforms map one to one to the colorscale 104 | colormap: { type: 't', value: null }, 105 | color_min: {type: 'f', value: 0.0}, 106 | color_max: {type: 'f', value: 1.0}, 107 | // the type of scale (linear/log) for x/y will be substituted in the shader 108 | // map from domain 109 | domain_x : { type: "2f", value: [0.0, 1.0] }, 110 | domain_y : { type: "2f", value: [0.0, 1.0] }, 111 | // to range (typically, pixel coordinates) 112 | range_x : { type: "2f", value: [0.0, 1.0] }, 113 | range_y : { type: "2f", value: [0.0, 1.0] }, 114 | // basically the corners of the image 115 | image_domain_x : { type: "2f", value: [0.0, 1.0] }, 116 | image_domain_y : { type: "2f", value: [0.0, 1.0] }, 117 | // extra opacity value 118 | opacity: {type: 'f', value: 1.0} 119 | }, 120 | vertexShader: require('raw-loader!../shaders/image-vertex.glsl').default, 121 | fragmentShader: require('raw-loader!../shaders/image-fragment.glsl').default, 122 | transparent: true, 123 | alphaTest: 0.01, // don't render almost fully transparant objects 124 | blending: THREE.CustomBlending, 125 | depthTest: false, 126 | depthWrite: false, 127 | 128 | // pre multiplied colors 129 | blendEquation: THREE.AddEquation, 130 | blendSrc: THREE.OneFactor, 131 | blendDst: THREE.OneMinusSrcAlphaFactor, 132 | 133 | blendEquationAlpha: THREE.AddEquation, 134 | blendSrcAlpha: THREE.OneFactor, 135 | blendDstAlpha: THREE.OneMinusSrcAlphaFactor, 136 | 137 | }); 138 | 139 | this.image_mesh = new THREE.Mesh(this.image_plane, this.image_material ); 140 | this.camera = new THREE.OrthographicCamera( 1 / - 2, 1 / 2, 1 / 2, 1 / - 2, -10000, 10000 ); 141 | this.camera.position.z = 10; 142 | this.scene = new THREE.Scene(); 143 | this.scene.add(this.image_mesh); 144 | 145 | return base_render_promise.then(() => { 146 | this.create_listeners(); 147 | this.update_minmax(); 148 | this.update_colormap(); 149 | this.update_opacity(); 150 | this.update_image(); 151 | this.update_scene(); 152 | this.listenTo(this.parent, "margin_updated", () => { 153 | this.update_scene(); 154 | this.draw(); 155 | }); 156 | }); 157 | } 158 | 159 | set_positional_scales() { 160 | var x_scale = this.scales.x, 161 | y_scale = this.scales.y; 162 | this.listenTo(x_scale, "domain_changed", function() { 163 | if (!this.model.dirty) { 164 | this.update_scene(); 165 | } 166 | }); 167 | this.listenTo(y_scale, "domain_changed", function() { 168 | if (!this.model.dirty) { 169 | this.update_scene(); 170 | } 171 | }); 172 | } 173 | 174 | set_ranges() { 175 | var x_scale = this.scales.x, 176 | y_scale = this.scales.y; 177 | if(x_scale) { 178 | x_scale.set_range(this.parent.padded_range("x", x_scale.model)); 179 | } 180 | if(y_scale) { 181 | y_scale.set_range(this.parent.padded_range("y", y_scale.model)); 182 | } 183 | } 184 | 185 | create_listeners() { 186 | super.create_listeners(); 187 | this.listenTo(this.model, "change:interpolation", () => { 188 | if(!this.texture) 189 | return; 190 | this.texture.magFilter = interpolations[this.model.get('interpolation')]; 191 | this.texture.minFilter = interpolations[this.model.get('interpolation')]; 192 | // it seems both of these need to be set before the filters have effect 193 | this.texture.needsUpdate = true; 194 | this.image_material.needsUpdate = true; 195 | this.update_scene(); 196 | }); 197 | var sync_visible = () => { 198 | this.image_material.visible = this.model.get('visible'); 199 | this.update_scene(); 200 | }; 201 | this.listenTo(this.model, "change:visible", sync_visible , this); 202 | sync_visible(); 203 | this.listenTo(this.model, "change:opacity", () => { 204 | this.update_opacity(); 205 | this.update_scene(); 206 | }, this); 207 | this.listenTo(this.scales.image.model, "domain_changed", () => { 208 | this.update_minmax(); 209 | this.update_scene(); 210 | }, this); 211 | this.listenTo(this.scales.image.model, "colors_changed", () => { 212 | this.update_colormap(); 213 | this.update_scene(); 214 | }, this); 215 | this.listenTo(this.scales.image.model, "domain_changed", this.update_image, this); 216 | this.listenTo(this.model, "change:image", () => { 217 | this.update_image(); 218 | this.update_scene(); 219 | }, this); 220 | this.listenTo(this.model, "data_updated", function() { 221 | //animate on data update 222 | var animate = true; 223 | this.update_scene(animate); 224 | }, this); 225 | } 226 | 227 | update_minmax() { 228 | var min = this.scales.image.model.get('min'); 229 | var max = this.scales.image.model.get('max'); 230 | if(typeof min !== 'number') 231 | min = 0; 232 | if(typeof max !== 'number') 233 | max = 0; 234 | this.image_material.uniforms.color_min.value = min; 235 | this.image_material.uniforms.color_max.value = max; 236 | } 237 | 238 | update_opacity() { 239 | this.image_material.uniforms.opacity.value = this.model.get('opacity'); 240 | } 241 | 242 | update_colormap() { 243 | 244 | var parse_color_string = function(string){ 245 | const hex_pattern = /.([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})/; 246 | const tuple_pattern = /rgb\((\d+), (\d+), (\d+)\)/; 247 | var hex_match = hex_pattern.exec(string); 248 | if (hex_match) { 249 | return [parseInt("0x" + hex_match[1]), 250 | parseInt("0x" + hex_match[2]), 251 | parseInt("0x" + hex_match[3])]; 252 | } 253 | var tuple_match = tuple_pattern.exec(string) 254 | if (tuple_match) { 255 | return [parseInt(tuple_match[1]), 256 | parseInt(tuple_match[2]), 257 | parseInt(tuple_match[3])]; 258 | } 259 | } 260 | 261 | // convert the d3 color scale to a texture 262 | var colors = this.scales.image.model.color_range; 263 | var color_scale = d3.scaleLinear() 264 | .range(colors) 265 | .domain(_.range(colors.length).map((i) => i/(colors.length-1))); 266 | var colormap_array = []; 267 | var N = 256; 268 | var colormap = _.map(_.range(N), (i) => { 269 | var index = i/(N-1); 270 | var rgb = color_scale(index); 271 | rgb = parse_color_string(rgb); 272 | colormap_array.push(rgb[0], rgb[1], rgb[2]); 273 | }); 274 | colormap_array = new Uint8Array(colormap_array); 275 | this.colormap_texture = new THREE.DataTexture(colormap_array, N, 1, THREE.RGBFormat, THREE.UnsignedByteType); 276 | this.colormap_texture.needsUpdate = true; 277 | this.image_material.uniforms.colormap.value = this.colormap_texture; 278 | } 279 | 280 | update_image(skip_render) { 281 | var image = this.model.get("image"); 282 | var type = null; 283 | var data = image.data; 284 | if(data instanceof Uint8Array) { 285 | type = THREE.UnsignedByteType; 286 | } else if(data instanceof Float64Array) { 287 | console.warn('ImageGLView.data is a Float64Array which WebGL does not support, will convert to a Float32Array (consider sending float32 data for better performance).'); 288 | data = Float32Array.from(data); 289 | type = THREE.FloatType; 290 | } else if(data instanceof Float32Array) { 291 | type = THREE.FloatType; 292 | } else { 293 | console.error('only types uint8 and float32 are supported'); 294 | return; 295 | } 296 | if(this.scales.image.model.get('scheme') && image.shape.length == 2) { 297 | if(this.texture) 298 | this.texture.dispose(); 299 | this.texture = new THREE.DataTexture(data, image.shape[1], image.shape[0], THREE.LuminanceFormat, type); 300 | this.texture.needsUpdate = true; 301 | this.image_material.uniforms.image.value = this.texture; 302 | this.image_material.defines.USE_COLORMAP = true; 303 | this.image_material.needsUpdate = true; 304 | } else if(image.shape.length == 3) { 305 | this.image_material.defines.USE_COLORMAP = false; 306 | if(this.texture) 307 | this.texture.dispose(); 308 | if(image.shape[2] == 3) 309 | this.texture = new THREE.DataTexture(data, image.shape[1], image.shape[0], THREE.RGBFormat, type); 310 | if(image.shape[2] == 4) 311 | this.texture = new THREE.DataTexture(data, image.shape[1], image.shape[0], THREE.RGBAFormat, type); 312 | this.texture.needsUpdate = true; 313 | this.image_material.uniforms.image.value = this.texture; 314 | } else { 315 | console.error('image data not understood'); 316 | } 317 | this.texture.magFilter = interpolations[this.model.get('interpolation')]; 318 | this.texture.minFilter = interpolations[this.model.get('interpolation')]; 319 | } 320 | 321 | update_scene(animate) { 322 | this.parent.update_gl(); 323 | } 324 | 325 | render_gl() { 326 | var fig = this.parent; 327 | var renderer = fig.renderer; 328 | var image = this.model.get("image"); 329 | 330 | var x_scale = this.scales.x ? this.scales.x : this.parent.scale_x; 331 | var y_scale = this.scales.y ? this.scales.y : this.parent.scale_y; 332 | 333 | // set the camera such that we work in pixel coordinates 334 | this.camera.left = 0; 335 | this.camera.right = fig.plotarea_width; 336 | this.camera.bottom = 0; 337 | this.camera.top = fig.plotarea_height; 338 | this.camera.updateProjectionMatrix(); 339 | 340 | var x = this.model.get('x'); 341 | var y = this.model.get('y'); 342 | var x0 = x[0], x1 = x[1]; 343 | var y0 = y[0], y1 = y[1]; 344 | var x0_pixel = x_scale.scale(x0), x1_pixel = x_scale.scale(x1); 345 | var y0_pixel = y_scale.scale(y0), y1_pixel = y_scale.scale(y1); 346 | 347 | var pixel_width = x1_pixel - x0_pixel; 348 | var pixel_height = y1_pixel - y0_pixel; 349 | this.image_mesh.position.set(x0_pixel + pixel_width/2, fig.plotarea_height - (y0_pixel + pixel_height/2), 0); 350 | this.image_mesh.scale.set(pixel_width, pixel_height, 1); 351 | 352 | this.image_material.uniforms.range_x.value = x_scale.scale.range(); 353 | // upside down in opengl 354 | this.image_material.uniforms.range_y.value = [y_scale.scale.range()[1], y_scale.scale.range()[0]]; 355 | this.image_material.uniforms.domain_x.value = x_scale.scale.domain(); 356 | this.image_material.uniforms.domain_y.value = y_scale.scale.domain(); 357 | 358 | this.image_material.uniforms.image_domain_x.value = [x0, x1]; 359 | this.image_material.uniforms.image_domain_y.value = [y0, y1]; 360 | 361 | renderer.render(this.scene, this.camera); 362 | var canvas = renderer.domElement; 363 | } 364 | 365 | relayout() { 366 | this.update_scene(); 367 | } 368 | 369 | draw(animate) { 370 | this.set_ranges(); 371 | 372 | } 373 | } 374 | 375 | 376 | var ImageMark = widgets.DOMWidgetModel.extend({ 377 | defaults: _.extend(widgets.DOMWidgetModel.prototype.defaults(), { 378 | _model_name : 'ImageMark', 379 | _view_name : 'HelloView', 380 | _model_module : 'bqplot-image-gl', 381 | _view_module : 'bqplot-image-gl', 382 | _model_module_version : version, 383 | _view_module_version : version, 384 | value : 'Hello World' 385 | }) 386 | }); 387 | 388 | 389 | export { 390 | ImageGLModel, ImageGLView 391 | }; 392 | -------------------------------------------------------------------------------- /examples/data.json: -------------------------------------------------------------------------------- 1 | {"width":87,"height":61,"values":[103,104,104,105,105,106,106,106,107,107,106,106,105,105,104,104,104,104,105,107,107,106,105,105,107,108,109,110,110,110,110,110,110,109,109,109,109,109,109,108,107,107,107,107,106,106,105,104,104,104,104,104,104,104,103,103,103,103,102,102,101,101,100,100,100,100,100,99,98,97,97,96,96,96,96,96,96,96,95,95,95,94,94,94,94,94,94,104,104,105,105,106,106,107,107,107,107,107,107,107,106,106,106,106,106,106,108,108,108,106,106,108,109,110,110,112,112,113,112,111,110,110,110,110,109,109,109,108,107,107,107,107,106,106,105,104,104,104,104,104,104,104,103,103,103,103,102,102,101,101,100,100,100,100,99,99,98,97,97,96,96,96,96,96,96,96,95,95,95,94,94,94,94,94,104,105,105,106,106,107,107,108,108,108,108,108,108,108,108,108,108,108,108,108,110,110,110,110,110,110,110,111,113,115,116,115,113,112,110,110,110,110,110,110,109,108,108,108,108,107,106,105,105,105,105,105,105,104,104,104,104,103,103,103,102,102,102,101,100,100,100,99,99,98,97,97,96,96,96,96,96,96,96,96,95,95,94,94,94,94,94,105,105,106,106,107,107,108,108,109,109,109,109,109,110,110,110,110,110,110,110,111,112,115,115,115,115,115,116,116,117,119,118,117,116,114,113,112,110,110,110,110,110,110,109,109,108,107,106,106,106,106,106,105,105,105,104,104,104,103,103,103,102,102,102,101,100,100,99,99,98,97,97,96,96,96,96,96,96,96,96,95,95,94,94,94,94,94,105,106,106,107,107,108,108,109,109,110,110,110,110,111,110,110,110,110,111,114,115,116,121,121,121,121,121,122,123,124,124,123,121,119,118,117,115,114,112,111,110,110,110,110,110,110,109,109,108,109,107,107,106,106,105,105,104,104,104,104,103,103,102,102,102,101,100,100,99,99,98,97,96,96,96,96,96,96,96,96,95,95,94,94,94,94,94,106,106,107,107,107,108,109,109,110,110,111,111,112,113,112,111,111,112,115,118,118,119,126,128,128,127,128,128,129,130,129,128,127,125,122,120,118,117,115,114,112,110,110,110,110,110,111,110,110,110,109,109,108,107,106,105,105,105,104,104,104,103,103,102,102,102,101,100,99,99,98,97,96,96,96,96,96,96,96,96,95,95,94,94,94,94,94,106,107,107,108,108,108,109,110,110,111,112,113,114,115,114,115,116,116,119,123,125,130,133,134,134,134,134,135,135,136,135,134,132,130,128,124,121,119,118,116,114,112,111,111,111,112,112,111,110,110,110,109,108,108,107,108,107,106,105,104,104,104,103,103,103,102,101,100,99,99,98,97,96,96,96,96,96,96,96,96,95,95,95,94,94,94,94,107,107,108,108,109,109,110,110,112,113,114,115,116,117,117,120,120,121,123,129,134,136,138,139,139,139,140,142,142,141,141,140,137,134,131,127,124,122,120,118,117,115,113,114,113,114,114,113,112,111,110,110,109,108,107,106,105,105,105,104,104,104,103,103,103,101,100,100,99,99,98,97,96,96,96,96,96,96,96,96,96,95,95,94,94,94,94,107,108,108,109,109,110,111,112,114,115,116,117,118,119,121,125,125,127,131,136,140,141,142,144,144,145,148,149,148,147,146,144,140,138,136,130,127,125,123,121,119,118,117,117,116,116,116,115,114,113,113,111,110,109,108,107,106,105,105,103,103,102,102,102,103,101,100,100,100,99,98,98,97,96,96,96,96,96,96,96,96,95,95,95,94,94,94,107,108,109,109,110,110,110,113,115,117,118,119,120,123,126,129,131,134,139,142,144,145,147,148,150,152,154,154,153,154,151,149,146,143,140,136,130,128,126,124,122,121,120,119,118,117,117,117,116,116,115,113,112,110,109,108,107,106,106,105,104,103,102,101,101,100,100,100,100,99,99,98,97,96,96,96,96,96,96,96,96,95,95,95,94,94,94,107,108,109,109,110,110,110,112,115,117,119,122,125,127,130,133,137,141,143,145,148,149,152,155,157,159,160,160,161,162,159,156,153,149,146,142,139,134,130,128,126,125,122,120,120,120,119,119,119,118,117,115,113,111,110,110,109,108,107,106,106,105,104,104,103,102,100,100,100,99,99,98,97,96,96,96,96,96,96,96,96,95,95,95,95,94,94,108,108,109,109,110,110,110,112,115,118,121,125,128,131,134,138,141,145,147,149,152,157,160,161,163,166,169,170,170,171,168,162,158,155,152,148,144,140,136,132,129,127,124,122,121,120,120,120,120,120,119,117,115,113,110,110,110,110,109,108,108,107,107,106,105,104,102,100,100,100,99,98,97,96,96,96,96,96,96,96,96,96,95,95,95,94,94,108,109,109,110,110,111,112,114,117,120,124,128,131,135,138,142,145,149,152,155,158,163,166,167,170,173,175,175,175,173,171,169,164,160,156,153,149,144,140,136,131,129,126,124,123,123,122,121,120,120,120,119,117,115,111,110,110,110,110,110,109,109,110,109,108,106,103,101,100,100,100,98,97,96,96,96,96,96,96,96,96,96,95,95,95,95,94,108,109,110,110,110,113,114,116,119,122,126,131,134,138,141,145,149,152,156,160,164,169,171,174,177,175,178,179,177,175,174,172,168,163,160,157,151,147,143,138,133,130,128,125,125,124,123,122,121,121,120,120,118,116,115,111,110,110,110,110,113,114,113,112,110,107,105,102,100,100,100,98,97,96,96,96,96,96,96,96,96,96,96,95,95,95,94,108,109,110,110,112,115,116,118,122,125,129,133,137,140,144,149,152,157,161,165,169,173,176,179,179,180,180,180,178,178,176,175,171,165,163,160,153,148,143,139,135,132,129,128,127,125,124,124,123,123,122,122,120,118,117,118,115,117,118,118,119,117,116,115,112,109,107,105,100,100,100,100,97,96,96,96,96,96,96,96,96,96,96,95,95,95,95,108,109,110,111,114,116,118,122,127,130,133,136,140,144,148,153,157,161,165,169,173,177,180,180,180,180,181,180,180,180,179,178,173,168,165,161,156,149,143,139,136,133,130,129,128,126,126,125,125,125,125,124,122,121,120,120,120,120,121,122,123,122,120,117,114,111,108,106,105,100,100,100,100,96,96,96,96,96,96,96,96,96,96,96,95,95,95,107,108,110,113,115,118,121,126,131,134,137,140,143,148,152,157,162,165,169,173,177,181,181,181,180,181,181,181,180,180,180,178,176,170,167,163,158,152,145,140,137,134,132,130,129,127,127,126,127,128,128,126,125,125,125,123,126,128,129,130,130,125,124,119,116,114,112,110,107,106,105,100,100,100,96,96,96,96,96,96,96,96,96,96,96,95,95,107,109,111,116,119,122,125,130,135,137,140,144,148,152,156,161,165,168,172,177,181,184,181,181,181,180,180,180,180,180,180,178,178,173,168,163,158,152,146,141,138,136,134,132,130,129,128,128,130,130,130,129,128,129,129,130,132,133,133,134,134,132,128,122,119,116,114,112,108,106,105,105,100,100,100,97,97,97,97,97,97,97,96,96,96,96,95,108,110,112,117,122,126,129,135,139,141,144,149,153,156,160,165,168,171,177,181,184,185,182,180,180,179,178,178,180,179,179,178,176,173,168,163,157,152,148,143,139,137,135,133,131,130,130,131,132,132,132,131,132,132,133,134,136,137,137,137,136,134,131,124,121,118,116,114,111,109,107,106,105,100,100,100,97,97,97,97,97,97,97,96,96,96,96,108,110,114,120,126,129,134,139,142,144,146,152,158,161,164,168,171,175,181,184,186,186,183,179,178,178,177,175,178,177,177,176,175,173,168,162,156,153,149,145,142,140,138,136,133,132,132,132,134,134,134,134,135,136,137,138,140,140,140,140,139,137,133,127,123,120,118,115,112,108,108,106,106,105,100,100,100,98,98,98,98,98,98,97,96,96,96,108,110,116,122,128,133,137,141,143,146,149,154,161,165,168,172,175,180,184,188,189,187,182,178,176,176,175,173,174,173,175,174,173,171,168,161,157,154,150,148,145,143,141,138,135,135,134,135,135,136,136,137,138,139,140,140,140,140,140,140,140,139,135,130,126,123,120,117,114,111,109,108,107,106,105,100,100,100,99,99,98,98,98,98,97,97,96,110,112,118,124,130,135,139,142,145,148,151,157,163,169,172,176,179,183,187,190,190,186,180,177,175,173,170,169,169,170,171,172,170,170,167,163,160,157,154,152,149,147,144,140,137,137,136,137,138,138,139,140,141,140,140,140,140,140,140,140,140,138,134,131,128,124,121,118,115,112,110,109,108,107,106,105,100,100,100,99,99,99,98,98,98,97,97,110,114,120,126,131,136,140,143,146,149,154,159,166,171,177,180,182,186,190,190,190,185,179,174,171,168,166,163,164,163,166,169,170,170,168,164,162,161,158,155,153,150,147,143,139,139,139,139,140,141,141,142,142,141,140,140,140,140,140,140,140,137,134,131,128,125,122,119,116,114,112,110,109,109,108,107,105,100,100,100,99,99,99,98,98,97,97,110,115,121,127,132,136,140,144,148,151,157,162,169,174,178,181,186,188,190,191,190,184,177,172,168,165,162,159,158,158,159,161,166,167,169,166,164,163,161,159,156,153,149,146,142,142,141,142,143,143,143,143,144,142,141,140,140,140,140,140,140,138,134,131,128,125,123,120,117,116,114,112,110,109,108,107,106,105,102,101,100,99,99,99,98,98,97,110,116,121,127,132,136,140,144,148,154,160,166,171,176,180,184,189,190,191,191,191,183,176,170,166,163,159,156,154,155,155,158,161,165,170,167,166,165,163,161,158,155,152,150,146,145,145,145,146,146,144,145,145,144,142,141,140,140,140,140,138,136,134,131,128,125,123,121,119,117,115,113,112,111,111,110,108,106,105,102,100,100,99,99,99,98,98,110,114,119,126,131,135,140,144,149,158,164,168,172,176,183,184,189,190,191,191,190,183,174,169,165,161,158,154,150,151,152,155,159,164,168,168,168,167,165,163,160,158,155,153,150,148,148,148,148,148,147,146,146,145,143,142,141,140,139,138,136,134,132,131,128,126,124,122,120,118,116,114,113,113,112,111,108,107,106,105,104,102,100,99,99,99,99,110,113,119,125,131,136,141,145,150,158,164,168,172,177,183,187,189,191,192,191,190,183,174,168,164,160,157,153,150,149,150,154,158,162,166,170,170,168,166,164,162,160,158,155,152,151,151,151,151,151,149,148,147,146,145,143,142,140,139,137,135,134,132,131,129,127,125,123,121,119,117,116,114,114,113,112,110,108,107,105,103,100,100,100,100,99,99,110,112,118,124,130,136,142,146,151,157,163,168,174,178,183,187,189,190,191,192,189,182,174,168,164,160,157,153,149,148,149,153,157,161,167,170,170,170,168,166,165,163,159,156,154,153,155,155,155,155,152,150,149,147,145,143,141,140,139,138,136,134,133,131,130,128,126,124,122,120,119,117,116,115,114,113,111,110,107,106,105,105,102,101,100,100,100,110,111,116,122,129,137,142,146,151,158,164,168,172,179,183,186,189,190,192,193,188,182,174,168,164,161,157,154,151,149,151,154,158,161,167,170,170,170,170,169,168,166,160,157,156,156,157,158,159,159,156,153,150,148,146,144,141,140,140,138,136,135,134,133,131,129,127,125,123,122,120,118,117,116,115,114,112,111,110,108,107,106,105,104,102,100,100,108,110,115,121,131,137,142,147,152,159,163,167,170,177,182,184,187,189,192,194,189,183,174,169,165,161,158,156,154,153,154,157,160,164,167,171,172,174,174,173,171,168,161,159,158,158,159,161,161,160,158,155,151,149,147,144,142,141,140,138,137,136,135,134,132,130,128,126,125,123,121,119,118,117,116,115,113,112,112,111,110,109,108,107,105,101,100,108,110,114,120,128,134,140,146,152,158,162,166,169,175,180,183,186,189,193,195,190,184,176,171,167,163,160,158,157,156,157,159,163,166,170,174,176,178,178,176,172,167,164,161,161,160,161,163,163,163,160,157,153,150,148,146,144,142,141,140,139,138,136,135,134,133,129,127,126,124,122,121,119,118,117,116,114,113,112,111,110,110,109,109,107,104,100,107,110,115,119,123,129,135,141,146,156,161,165,168,173,179,182,186,189,193,194,191,184,179,175,170,166,162,161,160,160,161,162,165,169,172,176,178,179,179,176,172,168,165,163,163,163,163,165,166,164,161,158,155,152,150,147,146,144,143,142,141,139,139,138,137,135,131,128,127,125,124,122,121,119,118,116,115,113,112,111,111,110,110,109,109,105,100,107,110,114,117,121,126,130,135,142,151,159,163,167,171,177,182,185,189,192,193,191,187,183,179,174,169,167,166,164,164,165,166,169,171,174,178,179,180,180,178,173,169,166,165,165,166,165,168,169,166,163,159,157,154,152,149,148,147,146,145,143,142,141,140,139,138,133,130,128,127,125,124,122,120,118,117,115,112,111,111,111,111,110,109,108,106,100,107,109,113,118,122,126,129,134,139,150,156,160,165,170,175,181,184,188,191,192,192,189,185,181,177,173,171,169,168,167,169,170,172,174,176,178,179,180,180,179,175,170,168,166,166,168,168,170,170,168,164,160,158,155,152,151,150,149,149,148,147,145,144,143,142,141,136,133,130,129,127,125,123,120,119,118,115,112,111,111,111,110,109,109,109,105,100,105,107,111,117,121,124,127,131,137,148,154,159,164,168,174,181,184,187,190,191,191,190,187,184,180,178,175,174,172,171,173,173,173,176,178,179,180,180,180,179,175,170,168,166,168,169,170,170,170,170,166,161,158,156,154,153,151,150,150,150,150,148,147,146,145,143,139,135,133,131,129,126,124,121,120,118,114,111,111,111,110,110,109,107,106,104,100,104,106,110,114,118,121,125,129,135,142,150,157,162,167,173,180,183,186,188,190,190,190,189,184,183,181,180,179,179,176,177,176,176,177,178,179,180,180,179,177,173,169,167,166,167,169,170,170,170,170,167,161,159,157,155,153,151,150,150,150,150,150,150,149,147,145,141,138,135,133,130,127,125,123,121,118,113,111,110,110,109,109,107,106,105,103,100,104,106,108,111,115,119,123,128,134,141,148,154,161,166,172,179,182,184,186,189,190,190,190,187,185,183,180,180,180,179,179,177,176,177,178,178,178,177,176,174,171,168,166,164,166,168,170,170,170,170,168,162,159,157,155,153,151,150,150,150,150,150,150,150,150,148,144,140,137,134,132,129,127,125,122,117,111,110,107,107,106,105,104,103,102,101,100,103,105,107,110,114,118,122,127,132,140,146,153,159,165,171,176,180,183,185,186,189,190,188,187,184,182,180,180,180,179,178,176,176,176,176,174,174,173,172,170,168,167,165,163,164,165,169,170,170,170,166,162,159,157,155,153,151,150,150,150,150,150,150,150,150,150,146,142,139,136,133,131,128,125,122,117,110,108,106,105,104,103,103,101,101,101,101,102,103,106,108,112,116,121,125,130,138,145,151,157,163,170,174,178,181,181,184,186,186,187,186,184,181,180,180,180,179,178,174,173,173,171,170,170,169,168,167,166,164,163,162,161,164,167,169,170,168,164,160,158,157,155,153,151,150,150,150,150,150,150,150,150,150,147,144,141,138,135,133,128,125,122,116,109,107,104,104,103,102,101,101,101,101,101,101,102,105,107,110,115,120,124,129,136,143,149,155,162,168,170,174,176,178,179,181,182,184,184,183,181,180,180,179,177,174,172,170,168,166,165,164,164,164,164,162,160,159,159,158,160,162,164,166,166,163,159,157,156,155,153,151,150,150,150,150,150,150,150,150,150,149,146,143,140,137,133,129,124,119,112,108,105,103,103,102,101,101,101,101,100,100,101,102,104,106,109,113,118,122,127,133,141,149,155,161,165,168,170,172,175,176,177,179,181,181,181,180,180,179,177,174,171,167,165,163,161,160,160,160,160,160,157,155,155,154,154,155,157,159,161,161,161,159,156,154,154,153,151,150,150,150,150,150,150,150,150,150,149,147,144,141,137,133,129,123,116,110,107,104,102,102,101,101,101,100,100,100,100,102,103,104,106,108,112,116,120,125,129,137,146,154,161,163,165,166,169,172,173,174,175,177,178,178,178,178,177,174,171,168,164,160,158,157,157,156,156,156,155,152,151,150,150,151,151,152,154,156,157,157,156,155,153,152,152,151,150,150,150,150,150,150,150,150,150,150,147,144,141,138,133,127,120,113,109,106,103,101,101,101,100,100,100,100,100,100,103,104,105,106,108,110,114,118,123,127,133,143,150,156,160,160,161,162,167,170,171,172,173,175,175,174,174,173,171,168,164,160,156,155,154,153,153,152,152,150,149,148,148,148,148,148,149,149,150,152,152,152,152,151,150,150,150,150,150,150,150,150,150,150,150,150,149,147,144,141,138,132,125,118,111,108,105,103,102,101,101,101,100,100,100,100,100,104,105,106,107,108,110,113,117,120,125,129,138,145,151,156,156,157,158,160,164,166,168,170,171,172,171,171,169,166,163,160,156,153,151,150,150,149,149,149,148,146,146,146,146,146,146,146,147,148,148,149,149,149,148,148,148,148,149,149,150,150,150,150,150,150,150,148,146,143,141,136,129,123,117,110,108,105,104,103,102,102,101,101,100,100,100,100,103,104,105,106,107,109,111,115,118,122,127,133,140,143,150,152,153,155,157,159,162,164,167,168,168,168,167,166,163,160,157,153,150,148,148,147,147,147,145,145,144,143,143,143,144,144,144,144,145,145,145,145,146,146,146,146,146,147,147,148,149,150,150,150,150,149,147,145,143,141,134,127,123,117,111,108,105,105,104,104,103,103,102,101,100,100,100,102,103,104,105,106,107,109,113,116,120,125,129,133,137,143,147,149,151,152,154,158,161,164,165,164,164,163,163,160,157,154,151,149,147,145,145,144,143,141,140,141,141,141,141,141,142,142,142,142,142,142,142,143,143,143,144,144,145,146,146,146,147,148,148,148,148,145,143,142,140,134,128,123,117,112,108,106,105,105,104,104,103,102,101,100,100,99,102,103,104,105,105,106,108,110,113,118,123,127,129,132,137,141,142,142,145,150,154,157,161,161,160,160,160,159,157,154,151,148,146,145,143,142,142,139,137,136,137,137,138,138,139,139,139,139,139,139,139,139,140,140,141,142,142,143,144,144,144,145,145,145,145,145,144,142,140,139,136,129,124,119,113,109,106,106,105,104,103,102,101,101,100,99,99,102,103,104,104,105,106,107,108,111,116,121,124,126,128,131,134,135,137,139,143,147,152,156,157,157,157,156,155,153,151,148,146,143,142,141,140,138,135,133,132,132,133,133,133,134,135,135,135,135,136,136,137,137,138,138,139,140,141,141,142,142,143,142,142,141,141,140,139,137,134,133,129,125,121,114,110,107,106,106,104,103,102,101,100,99,99,99,102,103,104,104,105,105,106,108,110,113,118,121,124,126,128,130,132,134,136,139,143,147,150,154,154,154,153,151,149,148,146,143,141,139,137,136,132,130,128,128,128,129,129,130,130,131,132,132,132,133,134,134,135,135,136,137,138,139,139,140,140,140,139,139,138,137,137,135,132,130,129,127,124,120,116,112,109,106,105,103,102,101,101,100,99,99,99,101,102,103,104,104,105,106,107,108,110,114,119,121,124,126,128,129,132,134,137,140,143,147,149,151,151,151,149,147,145,143,141,138,136,134,131,128,126,124,125,125,126,126,127,128,128,129,129,130,130,131,131,132,132,133,134,135,135,136,136,137,137,136,136,135,134,133,131,129,128,127,126,123,119,115,111,109,107,105,104,103,102,101,100,100,100,99,101,102,103,103,104,104,105,106,108,110,112,116,119,121,124,125,127,130,132,135,137,140,143,147,149,149,149,147,145,143,141,139,136,133,131,128,125,122,121,122,122,122,123,125,125,126,127,127,127,128,128,128,129,129,130,131,131,132,132,133,133,133,132,132,131,131,130,129,128,126,125,124,121,117,111,109,108,106,105,104,103,102,101,101,100,100,100,100,101,102,103,103,104,105,106,107,108,110,114,117,119,121,123,126,128,130,133,136,139,141,144,146,147,146,145,143,141,138,136,133,130,127,124,121,120,120,120,120,120,121,122,123,124,124,125,125,126,126,125,126,126,126,125,126,127,128,128,129,129,128,128,128,128,128,128,126,125,123,122,119,114,109,108,107,106,105,104,103,103,102,102,101,100,100,100,101,102,103,104,105,106,107,108,109,110,112,115,117,120,122,125,127,130,132,135,137,139,142,144,144,144,142,140,138,136,132,129,126,123,120,120,119,119,118,119,119,120,120,120,121,122,122,123,123,123,123,122,123,122,122,121,122,122,122,123,123,123,124,125,125,126,126,125,124,122,120,116,113,109,107,106,105,104,104,103,102,102,101,101,100,100,100,101,102,103,104,105,106,107,108,109,110,112,114,117,119,122,124,127,129,131,134,136,138,140,142,142,142,140,138,136,133,129,125,122,120,119,118,118,117,116,117,117,118,119,119,120,120,120,121,121,121,122,121,120,120,120,119,119,120,120,120,120,120,120,123,123,124,124,124,123,121,119,114,112,108,106,106,104,104,103,102,102,101,101,100,100,99,101,102,103,104,105,106,107,108,109,110,111,113,114,116,119,121,124,126,128,130,133,135,137,138,140,140,139,137,135,133,131,127,122,120,118,118,117,117,116,115,116,116,117,118,118,118,119,119,120,120,121,121,120,119,119,118,117,117,118,119,118,118,118,119,120,122,123,123,123,122,120,117,113,110,108,106,105,104,103,103,102,101,101,100,100,99,99,101,102,103,104,105,106,107,108,109,110,111,111,113,115,118,121,123,125,127,129,131,133,135,137,138,138,137,134,132,130,127,122,120,118,116,116,116,116,115,113,114,115,116,117,117,118,118,119,119,119,120,120,119,118,117,117,116,116,117,117,117,118,119,119,119,120,121,121,121,121,119,116,113,110,107,105,105,103,103,103,102,101,100,100,99,99,99,101,102,103,104,105,106,107,108,109,110,111,112,114,116,117,120,122,124,126,129,130,132,133,135,136,136,134,132,129,126,122,120,118,116,114,114,114,114,114,113,113,114,115,116,116,117,117,117,118,118,119,119,118,117,116,116,115,115,116,116,116,117,117,118,118,119,120,120,120,120,119,116,113,109,106,104,104,103,102,102,101,101,100,99,99,99,98,101,102,103,104,105,106,107,108,109,110,111,113,115,117,117,118,121,123,126,128,130,130,131,132,133,134,131,129,125,122,120,118,116,114,113,112,112,113,112,112,111,112,113,113,114,115,116,116,117,117,118,118,116,116,115,115,115,114,114,115,116,116,117,117,118,118,119,119,120,120,117,115,112,108,106,104,103,102,102,102,101,100,99,99,99,98,98,101,102,103,104,105,105,106,107,108,109,110,111,113,115,117,118,120,122,125,126,127,128,129,130,131,131,128,125,121,120,118,116,114,113,113,111,111,111,111,110,109,110,111,112,113,113,114,115,115,116,117,117,116,115,114,114,113,113,114,114,115,115,116,116,117,118,118,119,119,118,116,114,112,108,105,103,103,102,101,101,100,100,99,99,98,98,97,100,101,102,103,104,105,106,107,108,109,110,110,111,113,115,118,120,121,122,124,125,125,126,127,128,127,124,121,120,118,116,114,113,112,112,110,109,109,108,108,108,109,110,111,112,112,113,114,114,115,116,116,115,114,113,112,112,113,113,114,114,115,115,116,116,117,117,118,118,117,115,113,111,107,105,103,102,101,101,100,100,100,99,99,98,98,97,100,101,102,103,104,105,105,106,107,108,109,110,110,111,114,116,118,120,120,121,122,122,123,124,123,123,120,118,117,115,114,115,113,111,110,109,108,108,107,107,107,108,109,110,111,111,112,113,113,114,115,115,114,113,112,111,111,112,112,112,113,114,114,115,115,116,116,117,117,116,114,112,109,106,104,102,101,100,100,99,99,99,99,98,98,97,97]} --------------------------------------------------------------------------------