├── .github
└── donation-button.png
├── .gitignore
├── DEVELOPMENT.md
├── LICENSE
├── README.md
├── build_scripts
├── README.md
├── build_linux.sh
└── build_macos.sh
├── croquis_fe
├── README
├── __init__.py
├── croquis_fe.json
└── static
│ ├── croquis_fe.css
│ └── croquis_loader.js
├── doc
├── croquis2_1.png
├── ex1.png
├── ex2.png
├── ex3.png
├── ex4.png
├── ex5.csv
├── ex5.png
├── ex6.csv
├── ex6.png
├── reference.md
├── tutorial.md
└── vscode.md
├── setup.py
└── src
├── CMakeLists.txt
├── croquis
├── __init__.py
├── axis_util.py
├── buf_util.py
├── color_util.py
├── comm.py
├── data_util.py
├── datatype_util.py
├── display.py
├── env_helper.py
├── fig_data.py
├── lib
│ ├── .gitignore
│ └── README
├── log_util.py
├── misc_util.py
├── plot.py
├── png_util.py
├── tests
│ ├── axis_util_test.py
│ └── data_util_test.py
└── thr_manager.py
├── csrc
└── croquis
│ ├── bitmap_buffer.cc
│ ├── bitmap_buffer.h
│ ├── buffer.cc
│ ├── buffer.h
│ ├── canvas.h
│ ├── constants.h
│ ├── figure_data.h
│ ├── freeform_line_data.cc
│ ├── grayscale_buffer.cc
│ ├── grayscale_buffer.h
│ ├── intersection_finder.cc
│ ├── intersection_finder.h
│ ├── line_algorithm.h
│ ├── message.cc
│ ├── message.h
│ ├── plotter.cc
│ ├── plotter.h
│ ├── pybind11_shim.cc
│ ├── rectangular_line_data.cc
│ ├── rgb_buffer.cc
│ ├── rgb_buffer.h
│ ├── task.h
│ ├── tests
│ ├── bitmap_buffer_test.cc
│ ├── grayscale_buffer_test.cc
│ ├── line_algorithm_test.cc
│ └── show_bitmap.py
│ ├── thr_manager.cc
│ ├── thr_manager.h
│ └── util
│ ├── avx_util.h
│ ├── clock.h
│ ├── color_util.h
│ ├── error_helper.cc
│ ├── error_helper.h
│ ├── logging.cc
│ ├── logging.h
│ ├── macros.h
│ ├── math.h
│ ├── myhash.h
│ ├── optional.h
│ ├── stl_container_util.h
│ ├── string_printf.cc
│ └── string_printf.h
├── doc
└── messages.txt
├── js
├── .eslintrc.js
├── .gitignore
├── README.md
├── axis_handler.js
├── canvas_mouse_handler.js
├── croquis_fe.css
├── croquis_loader.js
├── css_helper.js
├── ctxt.js
├── event_replayer.js
├── label.js
├── main.js
├── tile.js
├── tile_handler.js
├── tile_set.js
├── util.js
└── webpack.config.js
└── ui_tests
├── .gitignore
├── run_all_tests.py
├── test_util
├── jupyter_launcher.py
└── test_helper.py
└── tests
├── basic_test.py
├── dimension_check_test.py
├── resize_test.py
└── save_test.py
/.github/donation-button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yongjik/croquis/c9f66db19fcf018c9655d957d110bcf8d1263a99/.github/donation-button.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | BAK
2 | *.BAK
3 | /build/
4 | /build.*/
5 | core
6 | core.*
7 | /croquis.egg-info
8 | /dist/
9 | dbg.log
10 | diff.txt
11 | /private
12 | __pycache__
13 | *.o
14 | Session.vim
15 | *.so
16 | stack.dump
17 | tags
18 | .vscode
19 | x.txt
20 | xxx
21 | test*.ipynb
22 | .ipynb_checkpoints
23 |
--------------------------------------------------------------------------------
/DEVELOPMENT.md:
--------------------------------------------------------------------------------
1 | # Building from the source
2 |
3 | The following should work on Linux and Mac OS. (Other OS's are not supported yet.)
4 |
5 | ## Prerequisites
6 |
7 | In order to build, you need [CMake](https://cmake.org/install/),
8 | [webpack](https://webpack.js.org/), [terser](https://github.com/terser/terser),
9 | and [pybind11](https://pybind11.readthedocs.io/en/stable/index.html). You can
10 | install webpack and terser by:
11 |
12 | ```
13 | npm install -g webpack webpack-cli terser
14 | ```
15 |
16 | (If don't like it, you don't *need* `-g`, but the commands `webpack` and
17 | `terser` must be in `$PATH` when you run the build.)
18 |
19 | You can install pybind11 by:
20 |
21 | ```
22 | # If you're using conda:
23 | conda install pybind11
24 |
25 | # Otherwise:
26 | pip3 install pybind11
27 | ```
28 |
29 | ## Building the wheel package
30 |
31 | ```
32 | cd (croquis top level directory)
33 | # Any directory name will do, except for "build", because setup.py uses it.
34 | mkdir build.make
35 | cd build.make
36 | cmake -G'Unix Makefiles' -DCMAKE_BUILD_TYPE=Release ../src
37 | make -j8 VERBOSE=1 wheel
38 | ```
39 |
40 | If you want to use a different compiler, add the compiler path to CMake argument
41 | (e.g., `-DCMAKE_CXX_COMPILER=/usr/local/bin/clang++`).
42 |
43 | If you want to use [Ninja](https://ninja-build.org/):
44 |
45 | ```
46 | cd (croquis top level directory)
47 | mkdir build.ninja
48 | cd build.ninja
49 | cmake -GNinja -DCMAKE_BUILD_TYPE=Release ../src
50 | ninja -v wheel
51 | ```
52 |
53 | After that, wheel file is available in the `dist` directory:
54 |
55 | ```
56 | cd (top level directory)/dist
57 | pip3 install croquis-0.1.0-cp39-cp39-linux_x86_64.whl # Or something similar.
58 | ```
59 |
60 | ## Testing at the source tree
61 |
62 | If you want to fiddle around, you can just build the C++ shared library by
63 | omitting `wheel` from the build commands. I.e.,
64 |
65 | ```
66 | cd (croquis top level directory)
67 | mkdir build.make
68 | cd build.make
69 | cmake -G'Unix Makefiles' -DCMAKE_BUILD_TYPE=Release ../src
70 | make -j8 VERBOSE=1
71 | ls ../src/croquis/lib
72 | # Will see something like: _csrc.cpython-39-x86_64-linux-gnu.so
73 | ```
74 |
75 | Use `-DCMAKE_BUILD_TYPE=Debug` to build in Debug mode.
76 |
77 | Use `make check` to run tests (there aren't many): for running tests, you also
78 | need to install [pytest](https://docs.pytest.org/).
79 |
80 | In addition, there are some integration tests under `src/ui_tests`: you can run
81 | them by `run_all_tests.py`. To run it, you first need to install
82 | [playwright](https://playwright.dev/python/).
83 |
84 | Now you can simply add `src` directory to your Python import path and do:
85 |
86 | ```
87 | import croquis
88 | ```
89 |
90 | In this way, croquis is working in the "dev environment", which will slightly
91 | tweak operations to (hopefully) make life easier:
92 |
93 | * We reload js/css file every time you restart the kernel.
94 | * Python/C++ code will leave logging on `dbg.log` (under the current directory).
95 |
96 | In the "dev environment", croquis also runs webpack at runtime(!) to create the
97 | js bundle that is then loaded by the browser, which means that webpack must be
98 | in your `PATH`.
99 |
100 | ## Packaging
101 |
102 | Python packaging is the dark underside of an otherwise great language that
103 | nobody speaks of in broad daylight. There's no need for **you** to know
104 | anything about it, as long as you want to just use croquis (or even modify and
105 | develop it!) - honestly I'm not sure if *I* understand it correctly.
106 |
107 | However, since I had trouble figuring it out, I wrote some
108 | [helper scripts and notes](build_scripts/README.md), in the hopes that it may be
109 | useful to future myself, or any other aspiring Pythonista who's thinking of
110 | building and distributing their own wonderful Python package.
111 |
112 | ## IDE integration
113 |
114 | Here are some [personal notes](doc/vscode.md) for using VS code (work in progress).
115 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2021 Yongjik Kim
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Croquis: plot graphs 100x faster on Jupyter Notebook
2 |
3 | Croquis is a lightweight Python library for drawing interactive graphs *really
4 | fast* on Jupyter Notebook. It lets you effortlessly browse and examine much
5 | larger data than other similar libraries.
6 |
7 | 
8 |
9 | **Install croquis by running:**
10 |
11 | ```
12 | pip install croquis
13 | ```
14 |
15 | 👉 Please look at the [tutorial](doc/tutorial.md) for more example.
16 |
17 | ---
18 |
19 | You might be wondering: there are already many [mature](https://matplotlib.org/)
20 | and [feature-rich](https://plotly.com/python/) plotting
21 | [libraries](https://bokeh.org/) — what's the appeal of a new, experimental
22 | library? Well, croquis is built with the following goals in mind:
23 |
24 | - **Fast:** Croquis contains a multi-threaded C++ plotting engine which
25 | automatically parallelizes graph generation, easily handling gigabytes
26 | of data.
27 | - **Simple:** Croquis provides a unified, simple API, regardless of data size.
28 | Whether your data contains ten data points or fifty million, you can make the
29 | same three function calls (or as many calls as you wish in a loop, if you
30 | prefer). The library will handle the rest.
31 | - **Interactive:** Since the C++ engine is alive as long as your figure is
32 | visible, at any moment you can zoom in, move around, or select a different
33 | subset of your data, and the figure will update accordingly.
34 |
35 | As an example,
36 | [here](https://github.com/yongjik/croquis-extra/tree/master/noaa_temperature_data)'s
37 | hourly ground temperature data of 2020 from the world's
38 | weather stations, downloaded from [NOAA website](https://www.ncdc.noaa.gov/isd/data-access).
39 | The data set contains 127 million points.
40 |
41 | https://user-images.githubusercontent.com/31876421/123535161-0b402d00-d6d7-11eb-9486-6218279eda9d.mp4
42 |
43 | The word "croquis" means [a quick, sketchy drawing](https://en.wikipedia.org/wiki/Croquis) -
44 | it's from French *croquis* which simply means "sketch." (The final -s is
45 | silent: it's French, after all.)
46 |
47 | ## Requirements
48 |
49 | - 64-bit Linux/Mac OS running on x86 with
50 | [AVX2](https://en.wikipedia.org/wiki/Advanced_Vector_Extensions#CPUs_with_AVX2)
51 | instruction set support. (Intel: Haswell (2013) or later; AMD: Excavator
52 | (2015) or later.)
53 | - Windows support is under work.
54 | - Sorry, other architectures aren't supported yet.
55 | - Python 3.6 or later.
56 | - Jupyter Notebook.
57 | - A modern browser (if it can run Jupyter Notebook, it's probably fine).
58 |
59 | ## How to install
60 |
61 | ```
62 | pip install croquis
63 | ```
64 |
65 | For building from the source, see [DEVELOPMENT.md](DEVELOPMENT.md).
66 |
67 | To test if it's working correctly, try this inside Jupyter Notebook:
68 |
69 | ```
70 | # Paste into a Jupyter cell.
71 |
72 | import croquis
73 | import numpy as np
74 |
75 | N = 1000000
76 | X = np.random.normal(size=(N, 1))
77 | Y = np.random.normal(size=(N, 1))
78 | labels=['pt %d' % i for i in range(N)]
79 |
80 | fig = croquis.plot()
81 | fig.add(X, Y, marker_size=3, labels=labels)
82 | fig.show()
83 | ```
84 |
85 | It should generate a plot like this:
86 |
87 | 
88 |
89 | For documentation, see the [tutorial](doc/tutorial.md) and the (very short)
90 | [reference](doc/reference.md).
91 |
92 | By the way, this library is of course open source (MIT License) and totally free
93 | to use, but just in case you really liked it for some reason, the author could
94 | use a cup of coffee or two... :)
95 |
96 | [](https://www.buymeacoffee.com/yongjikkim)
97 |
98 | ## Limitations
99 |
100 | Croquis is still experimental: as of version 0.1, we only support the **absolute
101 | bare minimum** functionality. In particular:
102 |
103 | - Only line plots are supported, nothing else: no bars, pie charts, heatmaps, etc.
104 | - All lines are solid: no dotted/dashed lines.
105 | - All markers are solid circles: no other shapes are currently supported.
106 | - No subplots: each Jupyter cell can contain only one graph.
107 | - Very few options to customize the plot. No titles, axis labels, or secondary axes.
108 | - No support for mobile browsers.
109 | - No dark mode.
110 | - As you can see, the UI is rather primitive.
111 |
112 | If croquis seems useful to you, but some features are missing for your use case,
113 | then please feel free to file an issue. (Of course I can't guarantee anything,
114 | but it will be nice to know that someone's interested.)
115 |
116 | ## FAQ
117 |
118 | ### Is it really 100 times faster?
119 |
120 | With large data, croquis can be *several hundred times* faster than other
121 | popular libraries. With very small data, there's less difference, as fixed-size
122 | overheads start to dominate.
123 |
124 | ### Can we use it outside of Jupyter Notebook?
125 |
126 | No, croquis is currently tied to Jupyter's message passing architecture, and all
127 | computation is done in the backend, so it needs an active Jupyter Python
128 | kernel.
129 |
130 | ### How does it work?
131 |
132 | Unlike most other similar libraries, croquis works by running a C++ "tile
133 | server," which computes fixed-sized "tiles" which is then sent back to the
134 | browser. If you have used Google Maps, the idea should be familiar. This has
135 | an important advantage:
136 |
137 | - The browser only has to know about tiles. Hence, the size of the data the
138 | browser needs to know is independent of the data set size.
139 |
140 | As a result, the browser stays lean and "snappy" even with massive data.
141 | (As explained in the [reference](doc/reference.md), we support `copy_data=False`
142 | option that even eliminates data copies altogether.)
143 |
144 | Moreover, unlike the browser's single-threaded javascript code, the C++-based
145 | tile server can draw multiple tiles in parallel, which allows even more speedup.
146 |
147 | (On the other hand, there are drawbacks - we have to basically re-implement every
148 | graph drawing algorithm inside this tile server, not being able to use any
149 | javascript API, except for very trivial things like coordinate grids.)
150 |
--------------------------------------------------------------------------------
/build_scripts/README.md:
--------------------------------------------------------------------------------
1 | This directory contains scripts for building and testing Python packages.
2 | (You probably don't need to look at this unless you're the package maintainer.
3 | Yes, you - you know who I'm talking about.)
4 |
5 | Currently there's no automatic release testing, CI/CD, or anything fancy:
6 | everything is manual.
7 |
8 | # Linux
9 |
10 | Build the wheel by running this [build_linux.sh](build_linux.sh), for example:
11 |
12 | ```
13 | ./build_linux.sh 3.8 0.1.0 master
14 | ```
15 |
16 | It will download the [manylinux](https://github.com/pypa/manylinux) docker
17 | image, run a build script inside it, and create a Python wheel (`.whl`) package
18 | file. The filename looks like: `croquis-0.1.0-cp38-cp38-linux_x86_64.whl`.
19 |
20 | You can create a test environment using conda.
21 |
22 | ```
23 | conda create -n env38 python=3.8
24 | conda activate env38
25 | conda install notebook jinja2 numpy
26 | pip install croquis-0.1.0-cp38-cp38-linux_x86_64.manylinux_2_12_x86_64.whl
27 | ```
28 |
29 | # Mac OS
30 |
31 | It seems like Anaconda's Python versions are built against Mac OS 10.9, which is
32 | old enough for building a distributable package. For example, Numpy (1.20.x)
33 | uses the same target.
34 |
35 | ```
36 | $ python -c 'from distutils.util import get_platform; print(get_platform())'
37 | macosx-10.9-x86_64
38 | ```
39 |
40 | So, you can run [build_macos.sh](build_macos.sh) inside conda environment -
41 | it will create a temporary conda environment (`PKG38` below), download necessary
42 | packages, and run the build inside it. (It's not as "hermetic" as Linux since
43 | it still uses the system C++ compiler, but I think this should be good enough
44 | now.)
45 |
46 | ```
47 | ./build_macos.sh 3.8 PKG38 0.1.0 master
48 | ```
49 |
50 | # Uploading package
51 |
52 | Use [twine](https://twine.readthedocs.io/en/latest/) to upload packages to PyPI.
53 | For testing in the "sandbox" (test.pypi.org), see
54 | [these instructions at python.org](https://packaging.python.org/tutorials/packaging-projects/#uploading-the-distribution-archives).
55 | That is:
56 |
57 | `twine upload -r testpypi *.whl`
58 |
59 | You will probably need to run it on least two machines, once to upload Linux
60 | packages and again for Mac packages. PyPI will automatically register them into
61 | the correct place. (That is, if you upload "version 0.1.0" three times, PyPI
62 | will only show one "version 0.1.0", not three different versions.) To check
63 | whether all the packages were correctly registered, see
64 | [https://test.pypi.org/simple/croquis/](https://test.pypi.org/simple/croquis/).
65 |
66 | I don't know what happens if you upload the "same" file twice - maybe it will
67 | overwrite the previous version?
68 |
69 | Obviously, remove `-r testpypi` (and be extra careful) when you're ready to
70 | upload the real thing.
71 |
--------------------------------------------------------------------------------
/build_scripts/build_linux.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | # Build wheel files for Linux, using the "manylinux" docker image.
4 | #
5 | # See: https://packaging.python.org/guides/packaging-binary-extensions/
6 | # https://github.com/pypa/manylinux
7 | #
8 | # How to use: build_linux.sh (python version) (package version) (git branch/tag)
9 | # e.g., build_linux.sh 3.8 0.1.0 master
10 |
11 | set -o errexit
12 | set -o pipefail
13 | set -o xtrace
14 |
15 | if [[ "$INSIDE_DOCKER" != "Y" ]]; then
16 | root_dir=$(dirname "$0")/..
17 | py_version="$1"
18 | pkg_version="$2"
19 | git_tag="$3"
20 | docker_container_name="crbuild-$pkg_version-$py_version"
21 |
22 | if [[ "$py_version" == "" || "$pkg_version" == "" || "$git_tag" == "" ]]; then
23 | set +o xtrace
24 | echo "How to use: build_linux.sh (python version) (package version) (git branch/tag)"
25 | echo "e.g., build_linux.sh 3.8 0.1.0 master"
26 | exit 1
27 | fi
28 |
29 | rm -rf .pkg_build || true
30 | mkdir .pkg_build
31 |
32 | # Create .zip file out of the whole source code.
33 | #
34 | # However, we copy a potentially "dirty" version of this script
35 | # (build_linux.sh) to run inside Docker, because otherwise it's just too
36 | # painful to debug this script itself.
37 | (
38 | cd "$root_dir"
39 | git archive --format=zip "$git_tag"
40 | ) > .pkg_build/croquis_src.zip
41 | cp "$0" .pkg_build
42 |
43 | # We now need node.js, which doesn't work on manylinux2010, so let's use
44 | # manylinux2014.
45 | #
46 | # (Well, we could try running node.js *outside* of Docker, and just copy the
47 | # js bundle file into Docker, but that's a lot of work, and I'm not sure if
48 | # anyone actually needs it ...)
49 | docker rm "$docker_container_name" || true
50 | docker pull quay.io/pypa/manylinux2014_x86_64
51 | docker run --name "$docker_container_name" \
52 | --mount type=bind,src="$PWD/.pkg_build",dst=/mnt/bind \
53 | quay.io/pypa/manylinux2014_x86_64:latest \
54 | bash -c "mkdir /build ; cd /build ; unzip /mnt/bind/croquis_src.zip ;
55 | INSIDE_DOCKER=Y /mnt/bind/build_linux.sh \
56 | '$py_version' '$pkg_version'"
57 |
58 | else
59 | # We're inside docker now.
60 | py_version="$1" # E.g., "3.8"
61 | py_short_version=$(echo "$py_version" | sed -e 's/\.//') # E.g., 38
62 | pkg_version="$2"
63 |
64 | # Directory names looks like: /opt/python/cp36-cp36m. /opt/python/cp310-cp310,
65 | # etc.
66 | #
67 | # Hmm not sure why globbing doesn't work, but let's use `find` ...
68 | pypath=$(find /opt/python -name "cp${py_short_version}-*")/bin
69 | export PATH="$pypath":"$PATH"
70 |
71 | # Install necessary components.
72 | pip3 install pybind11
73 |
74 | curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
75 | export NVM_DIR="$HOME/.nvm"
76 | set +o xtrace
77 | . $NVM_DIR/nvm.sh
78 | nvm install node
79 | set -o xtrace
80 | npm install -g webpack webpack-cli terser
81 |
82 | # Now build!
83 | mkdir -p /build/build.make
84 | cd /build/build.make
85 | cmake -G'Unix Makefiles' -DCMAKE_BUILD_TYPE=Release ../src
86 | CR_PKG_VERSION="$pkg_version" make -j8 VERBOSE=1 wheel
87 |
88 | # cp ../dist/croquis-$pkg_version-*.whl /mnt/bind/
89 |
90 | # Add manylinux tags to the package.
91 | auditwheel repair ../dist/croquis-$pkg_version-*.whl
92 | cp wheelhouse/croquis-$pkg_version-*.whl /mnt/bind/
93 |
94 | exit 0
95 | fi
96 |
97 | # We're back outside.
98 | docker container rm "$docker_container_name"
99 | pkg_file=$(find .pkg_build -name "croquis-$pkg_version-*.whl")
100 | cp "$pkg_file" .
101 | rm -rf .pkg_build
102 |
103 | set +o xtrace
104 | echo "Created package file: "$(basename "$pkg_file")
105 |
--------------------------------------------------------------------------------
/build_scripts/build_macos.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | # Build wheel files for Mac OS, using Anaconda.
4 | #
5 | # See: https://packaging.python.org/guides/packaging-binary-extensions/
6 | # https://github.com/MacPython/wiki/wiki/Spinning-wheels
7 | #
8 | # The `conda` command should be in PATH.
9 | #
10 | # How to use: build_linux.sh (python version) (conda env name to create) (package version) (git branch/tag)
11 | # e.g., build_linux.sh 3.8 PKG38 0.1.0 master
12 |
13 | if [[ $(uname -s) != "Darwin" ]]; then
14 | echo "This script is for Mac OS."
15 | exit 1
16 | fi
17 |
18 | set -o errexit
19 | set -o pipefail
20 | set -o xtrace
21 |
22 | if [[ "$INSIDE_BUILD_ENV" != "Y" ]]; then
23 | root_dir=$(dirname "$0")/..
24 | py_version="$1"
25 | conda_env="$2"
26 | pkg_version="$3"
27 | git_tag="$4"
28 |
29 | if [[ "$py_version" == "" || "$conda_env" == "" || "$pkg_version" == "" || "$git_tag" == "" ]]; then
30 | set +o xtrace
31 | echo "How to use: build_linux.sh (python version) (conda env name to create) (package version) (git branch/tag)"
32 | echo "e.g., build_linux.sh 3.8 PKG38 0.1.0 master"
33 | exit 1
34 | fi
35 |
36 | conda run -n "$conda_env" true && {
37 | set +o xtrace
38 | echo "Conda environment $conda_env already exists."
39 | echo "Please specify a non-existent environment name!"
40 | echo "Or remove the environment by running: conda env remove -n $conda_env"
41 | exit 1
42 | }
43 |
44 | rm -rf .pkg_build || true
45 | mkdir .pkg_build
46 |
47 | # Create .zip file out of the whole source code.
48 | #
49 | # However, we copy a potentially "dirty" version of this script
50 | # (build_linux.sh) to run inside Docker, because otherwise it's just too
51 | # painful to debug this script itself.
52 | (
53 | cd "$root_dir"
54 | git archive --format=zip "$git_tag"
55 | ) > .pkg_build/croquis_src.zip
56 | cp "$0" .pkg_build
57 |
58 | conda create --yes -n "$conda_env" python="$py_version" cmake pybind11
59 | conda run -n "$conda_env" --no-capture-output \
60 | --cwd "$PWD/.pkg_build" \
61 | bash -c "mkdir build ; cd build ; unzip ../croquis_src.zip ;
62 | INSIDE_BUILD_ENV=Y ../build_macos.sh \
63 | '$py_version' '$pkg_version'"
64 |
65 | else
66 | echo $PATH
67 | echo prefix = $CONDA_PREFIX
68 |
69 | # We're inside the build environment now.
70 | py_version="$1" # E.g., "3.8"
71 | py_short_version=$(echo "$py_version" | sed -e 's/\.//') # E.g., 38
72 | pkg_version="$2"
73 |
74 | python -V 2>&1 | grep -qF "Python $py_version" || {
75 | echo "Python version mismatch: expected $py_version, got $(python -V)"
76 | exit 1
77 | }
78 |
79 | # Now build!
80 | mkdir build.make
81 | cd build.make
82 | # We need -DPython3_FIND_VIRTUALENV=ONLY because otherwise CMake tries to
83 | # find other random Python versions hanging around in the system... -_-;;
84 | cmake -G'Unix Makefiles' -DCMAKE_BUILD_TYPE=Release \
85 | -DPython3_FIND_VIRTUALENV=ONLY \
86 | ../src
87 | CR_PKG_VERSION="$pkg_version" make -j8 VERBOSE=1 wheel
88 |
89 | exit 0
90 | fi
91 |
92 | # We're back outside.
93 | conda env remove -n "$conda_env"
94 | pkg_file=$(find .pkg_build/build/dist -name "croquis-$pkg_version-*.whl")
95 | cp "$pkg_file" .
96 | rm -rf .pkg_build
97 |
98 | set +o xtrace
99 | echo "Created package file: "$(basename "$pkg_file")
100 |
--------------------------------------------------------------------------------
/croquis_fe/README:
--------------------------------------------------------------------------------
1 | Support module for registering js/css files to Jupyter on package installation.
2 | Files are copied when building the package.
3 |
4 | (We may not need a separate module to do this, but I couldn't figure out a
5 | better way.)
6 |
7 | See setup.py for details.
8 |
--------------------------------------------------------------------------------
/croquis_fe/__init__.py:
--------------------------------------------------------------------------------
1 | def _jupyter_nbextension_paths():
2 | return [
3 | {
4 | 'section': 'notebook',
5 | 'src': 'static',
6 | 'dest': 'croquis_fe',
7 | 'require': 'croquis_fe/static',
8 | }
9 | ]
10 |
--------------------------------------------------------------------------------
/croquis_fe/croquis_fe.json:
--------------------------------------------------------------------------------
1 | {
2 | "load_extensions": {
3 | "croquis_fe/croquis_fe": true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/croquis_fe/static/croquis_fe.css:
--------------------------------------------------------------------------------
1 | ../../src/js/croquis_fe.css
--------------------------------------------------------------------------------
/croquis_fe/static/croquis_loader.js:
--------------------------------------------------------------------------------
1 | ../../src/js/croquis_loader.js
--------------------------------------------------------------------------------
/doc/croquis2_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yongjik/croquis/c9f66db19fcf018c9655d957d110bcf8d1263a99/doc/croquis2_1.png
--------------------------------------------------------------------------------
/doc/ex1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yongjik/croquis/c9f66db19fcf018c9655d957d110bcf8d1263a99/doc/ex1.png
--------------------------------------------------------------------------------
/doc/ex2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yongjik/croquis/c9f66db19fcf018c9655d957d110bcf8d1263a99/doc/ex2.png
--------------------------------------------------------------------------------
/doc/ex3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yongjik/croquis/c9f66db19fcf018c9655d957d110bcf8d1263a99/doc/ex3.png
--------------------------------------------------------------------------------
/doc/ex4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yongjik/croquis/c9f66db19fcf018c9655d957d110bcf8d1263a99/doc/ex4.png
--------------------------------------------------------------------------------
/doc/ex5.csv:
--------------------------------------------------------------------------------
1 | x,y1,y2
2 | 0,2,4
3 | 1,2.2,3.4
4 | 2,2.4,3.2
5 | 3,2.6,2.8
6 | 4,2.8,2.4
7 | 5,3,2
8 |
--------------------------------------------------------------------------------
/doc/ex5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yongjik/croquis/c9f66db19fcf018c9655d957d110bcf8d1263a99/doc/ex5.png
--------------------------------------------------------------------------------
/doc/ex6.csv:
--------------------------------------------------------------------------------
1 | date,location,sales
2 | 2021-05-01,Seoul,12
3 | 2021-05-02,Seoul,6
4 | 2021-05-03,Seoul,3
5 | 2021-05-04,Seoul,8
6 | 2021-05-05,Seoul,8
7 | 2021-05-06,Seoul,4
8 | 2021-05-07,Seoul,11
9 | 2021-05-08,Seoul,10
10 | 2021-05-09,Seoul,4
11 | 2021-05-10,Seoul,6
12 | 2021-05-11,Seoul,12
13 | 2021-05-12,Seoul,14
14 | 2021-05-13,Seoul,14
15 | 2021-05-14,Seoul,14
16 | 2021-05-15,Seoul,7
17 | 2021-05-16,Seoul,9
18 | 2021-05-17,Seoul,7
19 | 2021-05-18,Seoul,7
20 | 2021-05-19,Seoul,7
21 | 2021-05-20,Seoul,7
22 | 2021-05-21,Seoul,11
23 | 2021-05-22,Seoul,3
24 | 2021-05-23,Seoul,5
25 | 2021-05-24,Seoul,7
26 | 2021-05-25,Seoul,13
27 | 2021-05-26,Seoul,11
28 | 2021-05-27,Seoul,3
29 | 2021-05-28,Seoul,9
30 | 2021-05-29,Seoul,6
31 | 2021-05-30,Seoul,12
32 | 2021-05-31,Seoul,5
33 | 2021-05-10,New York,18
34 | 2021-05-11,New York,23
35 | 2021-05-12,New York,12
36 | 2021-05-13,New York,22
37 | 2021-05-14,New York,10
38 | 2021-05-15,New York,16
39 | 2021-05-16,New York,25
40 | 2021-05-17,New York,17
41 | 2021-05-18,New York,13
42 | 2021-05-19,New York,10
43 | 2021-05-20,New York,27
44 | 2021-05-21,New York,17
45 | 2021-05-22,New York,25
46 | 2021-05-23,New York,12
47 | 2021-05-24,New York,16
48 | 2021-05-25,New York,9
49 | 2021-05-26,New York,20
50 | 2021-05-27,New York,13
51 | 2021-05-28,New York,9
52 | 2021-05-29,New York,23
53 | 2021-05-30,New York,27
54 | 2021-05-31,New York,12
55 | 2021-04-25,Tokyo,15
56 | 2021-04-26,Tokyo,16
57 | 2021-04-27,Tokyo,13
58 | 2021-04-28,Tokyo,21
59 | 2021-04-29,Tokyo,23
60 | 2021-04-30,Tokyo,16
61 | 2021-05-01,Tokyo,12
62 | 2021-05-02,Tokyo,9
63 | 2021-05-03,Tokyo,9
64 | 2021-05-04,Tokyo,9
65 | 2021-05-05,Tokyo,24
66 |
--------------------------------------------------------------------------------
/doc/ex6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yongjik/croquis/c9f66db19fcf018c9655d957d110bcf8d1263a99/doc/ex6.png
--------------------------------------------------------------------------------
/doc/reference.md:
--------------------------------------------------------------------------------
1 | # Croquis API
2 |
3 | Since it's currently very simple, there's not much to document. Every graph is
4 | created by running the following commands:
5 |
6 | ```
7 | # Create the figure object.
8 | fig = croquis.plot(...)
9 |
10 | # Add one or more data sets.
11 | fig.add(x1, y1, ...)
12 | fig.add(x2, y2, ...)
13 |
14 | # Generate the figure.
15 | fig.show()
16 | ```
17 |
18 | ### `croquis.plot()`
19 |
20 | This function currently has only two optional arguments:
21 |
22 | * `x_axis='linear'` (**default**) interprets the x axis as ordinary numbers.
23 | * `x_axis='timestamp'` interprets it as [POSIX timestamps](https://en.wikipedia.org/wiki/Unix_time).
24 | Timestamps are interpreted as UTC but displayed using the local timezone.
25 | * `y_axis` behaves similarly.
26 |
27 | ### `fig.add(X, Y, colors=None, **kwargs)`
28 |
29 | Each call may add a set of lines - scatter plots can be generated by adding
30 | "lines" with a single point each, or by setting `line_width` to zero. You can
31 | call this function as many times as you want, but for best performance, it's
32 | better to batch multiple lines into a single call to `fig.add()`.
33 |
34 | * `X`, `Y`: Data points.
35 |
36 | Can be a Numpy array or something that can be transformed into a Numpy array.
37 | Data types can be any of: 8/16/32/64-bit signed/unsigned integer, `float`
38 | (i.e., `numpy.float32`), `double` (i.e., `numpy.float64`), or `np.datetime64`
39 | (which is converted to POSIX timestamps - specify `x_axis='linear'` to view
40 | them as time).
41 |
42 | If dimension is `N` or `(1, N)` (for some `N`), then it is considered `N`
43 | points of a single line. If dimension is `(M, N)` then it is considered `M`
44 | different lines, each with `N` data points.
45 |
46 | Broadcasting is supported, that is, one of `X` or `Y` can have dimension `N`
47 | or `(1, N)` and the other can have dimension `(M, N)` (where `M > 1`).
48 |
49 | For example, the following are supported combinations.
50 |
51 | ```
52 | fig.add([1, 2, 3], [4, 5, 6]) # Adds a single line with three points.
53 |
54 | X = np.linespace(0, 10, 0.1)
55 | fig.add(X, np.sin(X)) # Adds a single line.
56 |
57 | X = np.array([[1, 2, 3, 4, 5], [2, 4, 6, 8, 10])
58 | fig.add(X, np.sin(X)) # Adds two lines, each with five points.
59 |
60 | # Broadcasting: adds 100 lines, each with 200 points.
61 | # All lines share the same 200 x-coordinates.
62 | X = np.linspace(0, 2 * np.pi, 200)
63 | freqs = np.linspace(0, 1, 100).reshape(100, 1)
64 | Y = np.sin(freqs * X) # matrix of size 100 x 200
65 | fig.add(X, Y)
66 | ```
67 |
68 | * `colors` (optional)
69 |
70 | If specified, must have dimension `(M, 3)`, where `M` is the number of lines.
71 | Can be either integer (range [0, 255]), or float (range [0.0, 1.0]).
72 |
73 | Currently, markers (if present) must have the same color as lines.
74 |
75 | * `marker_size`, `line_width`, `highlight_line_width` (optional)
76 |
77 | Specifies the size of the marker, line width, and line width when a line is
78 | highlighted (by hovering). Note that only circles are supported so far.
79 |
80 | * `copy_data` (optional: default is `True`)
81 |
82 | If `False`, then croquis does not copy the arguments and instead holds a
83 | reference to the data. Can reduce memory usage for huge data, but if the
84 | underlying data is modified while the graph is being used, bad things may
85 | happen.
86 |
87 | * `start_idxs` (optional)
88 |
89 | Used for adding a batch of lines with an irregular number of points. If
90 | specified, changes how we interpret `X` and `Y`: `X` and `Y` must be 1-D
91 | arrays with the same length, and `start_idxs` must be a monotonically
92 | increasing integer array indicating where each line starts. For example:
93 |
94 | ```
95 | # Contains three lines, with 3, 5, and 4 points each.
96 | X = [0, 1, 2, 0, 1, 2, 3, 4, 0, 1, 2, 3]
97 | Y = [1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3]
98 | # | | |
99 | # line #0: 0 1 2 | |
100 | # #1: 3 4 5 6 7 |
101 | # #2: 8 9 10 11
102 | start_idxs = [0, 3, 8]
103 |
104 | fig.add(X, Y, start_idxs=start_idxs)
105 | ```
106 |
107 | * `groupby` (optional)
108 |
109 | (Not included in 0.1.0 release.)
110 |
111 | An alternative for `start_idxs`: when given, `X`, `Y`, and `groupby` must be
112 | 1-D arrays with the same size. The elements of `groupby` can be of any type,
113 | including string. Each line is made of points that share the same value of
114 | `groupby` - lines are arranged in the order of the first occurrence of the
115 | point.
116 |
117 | In addition, if `label`/`labels` are not given, the values of `groupby` are
118 | also used as labels.
119 |
120 | For example:
121 |
122 | ```
123 | # Contains four lines, appearing in this order:
124 | # 'ICN' (3 points)
125 | # 'SFO' (1 point)
126 | # 'GMP' (2 points)
127 | # 'RDU' (3 points)
128 | X = [ 0, 1, 2, 3, 4, 5, 6, 7, 8]
129 | Y = [ 10, 11, 20, 12, 30, 40, 41, 31, 42]
130 | airport = ['ICN', 'ICN', 'SFO', 'ICN', 'GMP', 'RDU', 'RDU', 'GMP', 'RDU']
131 |
132 | fig.add(X, Y, groupby=airport, marker_size=10)
133 | ```
134 |
135 | NOTE: `groupby` internally works by calling `numpy.unique()` and re-arranging
136 | the data, so it is less efficient than calling `start_idxs`.
137 |
138 | * `labels` (optional)
139 |
140 | If given, specifies the name of each line. Must be a list of strings (or
141 | something convertible to that) and have the same number of strings as lines.
142 |
143 | * `label` (optional)
144 |
145 | Shorthand for the case when there's only one line: must be a string.
146 | (Obviously, cannot be used together with `labels`.)
147 |
--------------------------------------------------------------------------------
/doc/vscode.md:
--------------------------------------------------------------------------------
1 | # Using VS Code
2 |
3 | I just started learning [VS code](https://code.visualstudio.com/), so this is a
4 | compilation of random stuff I found: some of them may be useful.
5 |
6 | ## Javascript
7 |
8 | Apparently, it seems impossible to run multiple debuggers (say, JS and Python)
9 | in one VS code window at the same time: see
10 | https://github.com/microsoft/vscode/issues/4507
11 |
12 | So, I guess the best solution is to create two workspaces, one under `src/js`
13 | and another for everything else. Then you can open two VS code windows side by
14 | side.
15 |
16 | For style check, you can install ESLint extension - default `.eslintrc.js` is
17 | provided.
18 |
19 | For debugging, start a separate terminal, go to any directory containing your
20 | notebook files, and run: `jupyter-notebook --no-browser`. (`--no-browser` is
21 | not necessary but it's cleaner that way.) It will print something like:
22 |
23 | ```
24 | $ jupyter-notebook --no-browser
25 | ......
26 | To access the notebook, open this file in a browser:
27 | file:///......
28 | Or copy and paste one of these URLs:
29 | http://localhost:8888/?token=5a279ac7f6b052b30a9905d665d8a36542218501d4e2c3f3
30 | or http://127.0.0.1:8888/?token=5a279ac7f6b052b30a9905d665d8a36542218501d4e2c3f3
31 | ```
32 |
33 | Inside VS code, choose "Start Debugging", choose "Chrome", and it will start a
34 | new instance of Chrome connected to the IDE.
35 |
36 | Inside VS Code, add the following `pathMapping` to `launch.json`, and restart
37 | debugging so that the change takes effect:
38 |
39 | ```
40 | "configurations": [
41 | {
42 | "type": "pwa-chrome",
43 | "request": "launch",
44 | "name": "Launch Chrome against localhost",
45 | ......
46 | "pathMapping": {
47 | "/nbextensions": "${workspaceFolder}",
48 | "/nbextensions/croquis_loader_dev.js":
49 | "${workspaceFolder}/croquis_loader.js",
50 | }
51 | }
52 | ]
53 | ```
54 |
55 | Now open the Notebook URL inside the debug Chrome.
56 |
57 | ## Python
58 |
59 | VS code can attach to a running process. First launch Jupyter notebook, run `ps
60 | -ef | grep ipykernel` to find out the process ID, and inside VS code, choose
61 | "Start Debugging" and then "Attach using Process ID". Add a breakpoint, execute
62 | a Jupyter cell in the browser, and the breakpoint will trigger.
63 |
64 | Inside `launch.json`, add `"justMyCode": false` so that you can see the full
65 | stack trace including Jupyter internals.
66 |
67 | For some reason, I found that "Go to Symbol in Workspace" is pretty unreliable -
68 | it started working only after I disabled pylance extension. YMMV.
69 |
70 | ## C++
71 |
72 | (TODO)
73 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # Intended to be invoked from cmake - don't run directly.
2 |
3 | import os
4 | import pathlib
5 | import setuptools
6 | import setuptools.command.build_ext
7 | import setuptools.command.build_py
8 | import shutil
9 | import subprocess
10 | import sys
11 |
12 | MODE = None
13 |
14 | curpath = pathlib.Path(os.path.dirname(os.path.realpath(__file__)))
15 | os.chdir(curpath)
16 |
17 | libpaths = None
18 | js_bundle_dir = None
19 |
20 | # Create my own build_ext class, which simply copies the file to the correct
21 | # location.
22 | # Inspired by: https://github.com/pybind/cmake_example/blob/master/setup.py
23 | class MyBuildExt(setuptools.command.build_ext.build_ext):
24 | def run(self):
25 | ext, = self.extensions
26 | assert len(libpaths) == 1, \
27 | 'Expected exactly one shared library file but found ' \
28 | f'{len(libpaths)}: ' + str(libpaths)
29 |
30 | destpath = self.get_ext_fullpath(ext.name)
31 | destdir = pathlib.Path(destpath).parent
32 | destdir.mkdir(parents=True, exist_ok=True)
33 | shutil.copy(src=libpaths[0], dst=destpath)
34 |
35 | # Also populate 'is_nodev.py' to let the library know that this is not a
36 | # development environment.
37 | with open(destdir / 'is_nodev.py', 'wt') as f:
38 | f.write(
39 | '''# This file doesn't exist in the source tree, and is generated by setup.py so
40 | # that the croquis library can tell if it's running inside a dev environment.
41 | pass
42 | ''')
43 |
44 | # Copy the generated js bundle files.
45 | # Copied from: https://digip.org/blog/2011/01/generating-data-files-in-setup.py.html
46 | class MyBuildPy(setuptools.command.build_py.build_py):
47 | def run(self):
48 | static_dir = pathlib.Path(self.build_lib) / 'croquis_fe' / 'static'
49 | self.mkpath(str(static_dir))
50 | for fn in 'croquis_fe.js', 'croquis_fe.js.map':
51 | shutil.copy(src=(js_bundle_dir / fn), dst=(static_dir / fn))
52 |
53 | super().run()
54 |
55 | if __name__ == '__main__':
56 | MODE = sys.argv[1] if len(sys.argv) >= 2 else None
57 |
58 | if MODE == '-CMAKE':
59 | print('*** setup.py invoked from cmake: ', ' '.join(sys.argv))
60 | del sys.argv[1]
61 | print('*** Cleaning build directory ...')
62 | if (curpath / 'build').exists():
63 | shutil.rmtree(curpath / 'build')
64 | libpaths = [sys.argv.pop(1)]
65 | js_bundle_dir = pathlib.Path(sys.argv.pop(1))
66 |
67 | elif MODE == '-f':
68 | print('*** setup.py invoked directly: ' +
69 | "I assume you know what you're doing ...")
70 | del sys.argv[1]
71 | libpaths = list(curpath.glob('src/croquis/lib/_csrc*'))
72 | js_bundle_dir = curpath / 'src' / 'js' / 'dist'
73 |
74 | else:
75 | print('''
76 | *** This setup.py is not supposed to be invoked directly.
77 | *** Instead, please generate the package via cmake:
78 |
79 | # You can use any directory for cmake, but don't use "build" because
80 | # setuptools will also try to use it!
81 | mkdir build.cmake
82 | cd build.cmake
83 | cmake -DCMAKE_BUILD_TYPE=Release ../src
84 | make wheel # or make VERBOSE=1 wheel
85 |
86 | *** If you *really* want to run setup.py directly, first build the C library via
87 | *** cmake, put the library file under src/croquis/lib/, and invoke setup.py with
88 | *** -f:
89 |
90 | python setup.py -f bdist_wheel # or some other command you want to run.
91 | ''')
92 | sys.exit(1)
93 |
94 | with open(curpath / 'README.md', 'rt') as f:
95 | long_description = f.read()
96 |
97 | csrc = setuptools.Extension(name='croquis.lib._csrc', sources=[])
98 |
99 | setuptools.setup(
100 | name='croquis',
101 | version=os.environ.get('CR_PKG_VERSION', '0.1.0'),
102 | author='Yongjik Kim',
103 | description='Library for plotting interactive graphs on Jupyter Notebook',
104 | long_description=long_description,
105 | long_description_content_type='text/markdown',
106 | url='https://github.com/yongjik/croquis',
107 | classifiers=[
108 | "Framework :: Jupyter",
109 | "Programming Language :: Python :: 3",
110 | "Programming Language :: Python :: 3.6",
111 | "Programming Language :: Python :: 3.7",
112 | "Programming Language :: Python :: 3.8",
113 | "Programming Language :: Python :: 3.9",
114 | "Programming Language :: Python :: Implementation :: CPython",
115 | "License :: OSI Approved :: MIT License",
116 | "Operating System :: MacOS :: MacOS X",
117 | "Operating System :: POSIX :: Linux",
118 | # "Operating System :: OS Independent",
119 | ],
120 | python_requires='>=3.6',
121 | install_requires=[
122 | 'jinja2',
123 |
124 | # Minimum version to support np.argsort(kind='stable').
125 | 'numpy>=1.15.0',
126 | ],
127 |
128 | ext_modules=[csrc],
129 | # package_data={'': ['lib/_csrc*']},
130 | cmdclass={
131 | 'build_ext': MyBuildExt,
132 | 'build_py': MyBuildPy,
133 | },
134 |
135 | #-----------------------------------------
136 | # Install nbextension at install time.
137 | # Basic idea is stolen from plotly/setup.py (version 4.14.3)
138 | # See also: https://jupyter-notebook.readthedocs.io/en/stable/examples/Notebook/Distributing%20Jupyter%20Extensions%20as%20Python%20Packages.html
139 |
140 | # Not completely sure, but it seems like we need a separate __init__.py
141 | # for Jupyter to run, so it's probably better to have a separate "fe"
142 | # module. (Plotly similary has `plotlywidget`.)
143 | packages=['croquis', 'croquis_fe'],
144 |
145 | package_dir={
146 | 'croquis': 'src/croquis',
147 | 'croquis_fe': 'croquis_fe',
148 | },
149 | package_data={
150 | 'croquis_fe': [
151 | # Symlinked to src/js.
152 | 'static/croquis_fe.css',
153 | 'static/croquis_loader.js',
154 |
155 | # croquis_fe.js & croquis_fe.js.map cannot be added like this,
156 | # because they're not in the correct directory. Instead, they
157 | # are added by MyBuildPy class.
158 | ],
159 | },
160 |
161 | # TODO: We're duplicating these files? (Once under croquis_fe/, and
162 | # then under share/jupyter/...)
163 | data_files=[
164 | (
165 | 'share/jupyter/nbextensions/croquis_fe', [
166 | 'src/js/croquis_fe.css',
167 | 'src/js/croquis_loader.js',
168 | str(js_bundle_dir / 'croquis_fe.js'),
169 | str(js_bundle_dir / 'croquis_fe.js.map'),
170 | ],
171 | ),
172 | (
173 | 'etc/jupyter/nbconfig/notebook.d',
174 | ['croquis_fe/croquis_fe.json'],
175 | ),
176 | ],
177 | zip_safe=False,
178 | )
179 |
--------------------------------------------------------------------------------
/src/croquis/__init__.py:
--------------------------------------------------------------------------------
1 | # Initial test module to test client-server communication.
2 |
3 | import sys
4 |
5 | # We use f-strings, so 3.6 is the minimum version.
6 | # TODO: Check if we can actually run on 3.6!
7 | assert sys.version_info[:2] >= (3, 6), 'Croquis requires Python 3.6 and higher!'
8 |
9 | import logging
10 | import os
11 |
12 | from . import env_helper, log_util
13 |
14 | # Check if we want to enable debug logging.
15 | def maybe_enable_dbglog():
16 | filename = None
17 |
18 | if env_helper.ENV in ['dev', 'unittest']:
19 | filename = 'dbg.log'
20 |
21 | s = os.environ.get('CROQUIS_DBGLOG')
22 | if s is not None: filename = s
23 |
24 | if filename is not None:
25 | log_util.begin_console_logging(logging.DEBUG, filename)
26 |
27 | maybe_enable_dbglog()
28 |
29 | if env_helper.ENV in ['dev', 'unittest']:
30 | # Enable Python stack dump in case we hit segfault.
31 | import faulthandler
32 | faulthandler.enable(file=open('stack.dump', 'a'))
33 |
34 | if env_helper.ENV in ['dev', 'deployed']:
35 | logger = logging.getLogger(__name__)
36 | logger.info('Loading croquis module ...')
37 |
38 | # Initialize worker threads.
39 | from .lib import _csrc
40 | from . import thr_manager
41 |
42 | # Now initialzie other parts.
43 | from . import comm
44 | if env_helper.has_ipython(): comm.comm_manager.open()
45 |
46 | # Import public API.
47 | # TODO: More functions here!!
48 | from .plot import plot
49 |
--------------------------------------------------------------------------------
/src/croquis/buf_util.py:
--------------------------------------------------------------------------------
1 | # Utility functions for handling buffers.
2 |
3 | import types
4 |
5 | import numpy as np
6 |
7 | # Return a memoryview object that represents `data`.
8 | # If `copy_data` is true, make a copy first.
9 | #
10 | # If a list is given, convert to numpy array first, using `dtype`. Otherwise,
11 | # `dtype` is unused.
12 | #
13 | # TODO: I'm not sure if using memoryview is useful. Since we need numpy anyway,
14 | # maybe we should just move everything to numpy arrays?
15 | def ensure_buffer(data, dtype=None, copy_data=True):
16 | # Sanity check.
17 | if isinstance(data, types.GeneratorType):
18 | raise TypeError(
19 | f'Cannot convert a generator {type(data)} to a buffer: '
20 | 'did you use (... for ... in ...) instead of [...]?')
21 |
22 | try:
23 | data = memoryview(data)
24 | except TypeError:
25 | # If `data` does not support Python buffer protocol (e.g., it is a
26 | # list), then we first convert it to a numpy array.
27 | #
28 | # cf. https://docs.python.org/3/c-api/buffer.html
29 | data = np.array(data, copy=copy_data, dtype=dtype)
30 |
31 | # Convert numpy datetime to unix timestamp.
32 | if np.issubdtype(data.dtype, np.datetime64):
33 | data = (data - np.datetime64(0, 's')) / np.timedelta64(1, 's')
34 |
35 | return memoryview(data)
36 |
37 | if copy_data:
38 | data = memoryview(np.copy(data))
39 | return data
40 |
--------------------------------------------------------------------------------
/src/croquis/color_util.py:
--------------------------------------------------------------------------------
1 | # Generator for the default color scheme.
2 | #
3 | # We may go "fancy" later, but for now, let's stick to the 90's feel.
4 |
5 | import numpy as np
6 |
7 | # The default color was created in a very "scientific" way by manually
8 | # generating a sequence of points in the HSV coordinate and tweaking it until it
9 | # sort of looked alright. I.e.,
10 | #
11 | #-------------------------------------------------
12 | # import matplotlib.colors
13 | # import numpy as np
14 | #
15 | # N = 20
16 | # mult = 7
17 | #
18 | # HSV = np.zeros((N, 3))
19 | #
20 | # # Give larger distance around green & purple.
21 | # HSV[:, 0] = [
22 | # 0, 1, 2, 3, 4, 5,
23 | # 7, 9, 11, 13, 15,
24 | # 16, 17, 18, 19, 20,
25 | # 22, 24, 26, 28,
26 | # ]
27 | # HSV[:, 0] /= 30
28 | #
29 | # HSV[:, 1] = 1
30 | # HSV[:, 2] = 1
31 | #
32 | # HSV[[1, 4, 6, 9, 12, 15, 18], 1] = 0.8
33 | # HSV[[1, 4, 6, 9, 12, 15, 18], 2] = 0.8
34 | # HSV[[2, 5, 7, 10, 13, 16, 19], 2] = 0.5
35 | # HSV[8, 2] = 0.3
36 | #
37 | # RGB = matplotlib.colors.hsv_to_rgb(HSV)
38 | #
39 | # for idx in range(N):
40 | # color = (RGB[(idx * mult) % N] * 255).astype(np.uint8)
41 | # print(' 0x%02x, 0x%02x, 0x%02x, # %d' % (tuple(color) + (idx,)))
42 | #-------------------------------------------------
43 |
44 | BLK_SIZE = 20
45 | DEFAULT_COLORS = np.array([
46 | 0xff, 0x00, 0x00, # 0
47 | 0x19, 0x7f, 0x00, # 1
48 | 0x00, 0x33, 0xff, # 2
49 | 0xcc, 0x49, 0x28, # 3
50 | 0x00, 0x4c, 0x0f, # 4
51 | 0x28, 0x28, 0xcc, # 5
52 | 0x7f, 0x33, 0x00, # 6
53 | 0x28, 0xcc, 0x8a, # 7
54 | 0x32, 0x00, 0x7f, # 8
55 | 0xff, 0x99, 0x00, # 9
56 | 0x00, 0x7f, 0x7f, # 10
57 | 0xcc, 0x00, 0xff, # 11
58 | 0xcc, 0xab, 0x28, # 12
59 | 0x00, 0xcb, 0xff, # 13
60 | 0xcc, 0x28, 0xab, # 14
61 | 0x7f, 0x7f, 0x00, # 15
62 | 0x28, 0x8a, 0xcc, # 16
63 | 0x7f, 0x00, 0x33, # 17
64 | 0x8a, 0xcc, 0x28, # 18
65 | 0x00, 0x33, 0x7f, # 19
66 | ], dtype=np.uint8).reshape(BLK_SIZE, 3)
67 |
68 | def default_colors(start_item_id, item_cnt):
69 | if item_cnt == 0: return np.zeros((0, 3), dtype=np.uint8)
70 |
71 | start_block = start_item_id // BLK_SIZE
72 | end_block = (start_item_id + item_cnt + BLK_SIZE - 1) // BLK_SIZE
73 | offset = start_item_id - start_block * BLK_SIZE
74 |
75 | C = np.tile(DEFAULT_COLORS, (end_block - start_block, 1))
76 | return C[offset:(offset + item_cnt), :]
77 |
--------------------------------------------------------------------------------
/src/croquis/comm.py:
--------------------------------------------------------------------------------
1 | # Communications support: see
2 | # https://jupyter-notebook.readthedocs.io/en/stable/comms.html
3 |
4 | import collections
5 | import logging
6 | import uuid
7 | import weakref
8 |
9 | logger = logging.getLogger(__name__)
10 |
11 | # Create a unique UUID so that FE can find out when BE restarts.
12 | BE_uuid = str(uuid.uuid4())
13 |
14 | class CommManager(object):
15 | def __init__(self):
16 | self.is_open = False
17 | self.comm = None
18 |
19 | self.plots = {}
20 |
21 | # handlers[canvas_id][msgtype] = weakref.WeakMethod(callback)
22 | self.handlers = collections.defaultdict(dict)
23 |
24 | # Called only if we're inside IPython: otherwise obviously there's no
25 | # communications channel!
26 | def open(self):
27 | logger.info('Opening communication channel ...')
28 | assert not self.is_open, 'Already opened!'
29 | get_ipython().kernel.comm_manager.register_target(
30 | 'croquis', self._open_handler)
31 | self.is_open = True
32 |
33 | # Register a Plotter object so that it's not garbage-collected when the user
34 | # loses the reference to it - which is quite common because people will just
35 | # reuse the variable `fig` in different cells.
36 | def register_plot(self, canvas_id, plot):
37 | self.plots[canvas_id] = plot
38 |
39 | # Register a handler for message received from FE. The callback is called
40 | # with `canvas_id`, `msgtype`, and json contents of the message.
41 | #
42 | # I'm not sure if I have to worry, but to make sure that objects are
43 | # properly garbage collected, we assume `callback` is a "bound method" and
44 | # create a weak reference to it, so that we don't accidentally hold a
45 | # reference to the underlying object.
46 | #
47 | # For sending messages out, see thr_manager.register_cpp_callback().
48 | def register_handler(self, canvas_id, msgtype, callback):
49 | ref = weakref.WeakMethod(callback)
50 | if msgtype in self.handlers[canvas_id]:
51 | logger.warn('Handler already registered for %s (%s) ...',
52 | canvas_id, msgtype)
53 | self.handlers[canvas_id][msgtype] = ref
54 |
55 | # if `msgtype` is None, deregister the whole cell.
56 | def deregister_handler(self, canvas_id, msgtype=None):
57 | if msgtype is None:
58 | del self.handlers[canvas_id]
59 | del self.plots[canvas_id]
60 | else:
61 | del self.handlers[canvas_id][msgtype]
62 |
63 | # Send a message. `attachments` is an optional list of memoryview objects
64 | # (or some objects that supports the buffer protocol).
65 | def send(self, canvas_id, msgtype, attachments=None, **kwargs):
66 | kwargs.update(canvas_id=canvas_id, msg=msgtype)
67 | if attachments is not None:
68 | if attachments == []:
69 | attachments = None
70 | elif type(attachments) != list:
71 | attachments = [attachments]
72 | logger.debug('CommManager.send() sending message: %s', kwargs)
73 | self.comm.send(kwargs, buffers=attachments)
74 |
75 | # Handler for the initial message.
76 | def _open_handler(self, comm, open_msg):
77 | logger.debug('Initial open packet received from client: ', open_msg)
78 | self.comm = comm
79 |
80 | comm.on_msg(self._msg_handler)
81 |
82 | logger.debug('Sending BE_ready packet ...')
83 | logger.debug(type(comm))
84 | comm.send({'msg': 'BE_ready'})
85 | logger.debug('Sent BE_ready packet ...')
86 |
87 | # Handler for all subsequent messages.
88 | # See also ThrManager._callback_entry_point().
89 | def _msg_handler(self, msg):
90 | logger.debug('Data received from client: %s', msg)
91 | try:
92 | data = msg['content']['data']
93 | canvas_id = data['canvas_id']
94 | msgtype = data['msg']
95 | except KeyError:
96 | logger.error('Malformed message: %s', msg)
97 | return
98 |
99 | if canvas_id not in self.handlers:
100 | logger.warning('Unrecognized canvas_id %s', canvas_id)
101 | return
102 |
103 | if msgtype not in self.handlers[canvas_id]:
104 | logger.warning('Missing handler for canvas_id=%s, msgtype=%s',
105 | canvas_id, msgtype)
106 | return
107 |
108 | callback = self.handlers[canvas_id][msgtype]
109 | cb = callback()
110 | if cb is None:
111 | logger.warning('Handler gone for canvas_id=%s, msgtype=%s',
112 | canvas_id, msgtype)
113 | del self.handlers[canvas_id][msgtype]
114 | return
115 |
116 | cb(canvas_id, msgtype, msg)
117 |
118 | comm_manager = CommManager()
119 |
--------------------------------------------------------------------------------
/src/croquis/data_util.py:
--------------------------------------------------------------------------------
1 | # Miscellaneous utility functions for shuffling data.
2 |
3 | import numpy as np
4 |
5 | # Given a numpy array `keys` where the same key may be repeated multiple times,
6 | # construct the following:
7 | #
8 | # - `unique_keys`: contains each key just once, in the order they appear first.
9 | # - `idxs`: a map for shuffling data in `keys` so that key values are grouped
10 | # together.
11 | # - `start_idxs`: contains the start index of each unique key in keys[idxs].
12 | #
13 | # For example if
14 | # keys == ['foo', 'foo', 'bar', 'foo', 'qux', 'baz', 'baz', 'qux', 'baz'],
15 | # 0 1 2 3 4 5 6 7 8
16 | # (total 9 rows with 4 unique keys)
17 | #
18 | # then the intermediate variables below are:
19 | # unique == ['bar', 'baz', 'qoo', 'qux']
20 | # idxs0 == [2 5 0 4]
21 | # inverse == [2 2 0 2 3 1 1 3 1]
22 | # counts == [1 3 3 2]
23 | #
24 | # order = [2 0 3 1]
25 | #
26 | # and the return values are:
27 | # unique_keys = ['foo', 'bar', 'qux', 'baz'] # in the order they appear
28 | # idxs = [0 1 3 2 4 7 5 6 8] # 0 1 3 == foo
29 | # # 2 == bar
30 | # # 4 7 == qux
31 | # # 5 6 8 == baz
32 | # start_idxs = [0, 3, 4, 6, (9)] # Only returns the first 4 numbers.
33 | def compute_groupby(keys):
34 | unique, idxs0, inverse, counts = np.unique(
35 | keys, return_index=True, return_inverse=True, return_counts=True)
36 |
37 | order = np.argsort(idxs0, kind='stable')
38 |
39 | try:
40 | # This will succeed if `keys` is a numpy array.
41 | unique_keys = keys[idxs0[order]]
42 | except TypeError:
43 | unique_keys = [keys[idxs0[i]] for i in order]
44 |
45 | idxs = np.argsort(idxs0[inverse], kind='stable')
46 |
47 | start_idxs = np.zeros(dtype=np.int64, shape=(len(unique_keys) + 1))
48 | np.cumsum(counts[order], out=start_idxs[1:])
49 |
50 | return unique_keys, idxs, start_idxs[:-1]
51 |
--------------------------------------------------------------------------------
/src/croquis/datatype_util.py:
--------------------------------------------------------------------------------
1 | # Utility functions for handling different input data types.
2 |
3 | import collections
4 | import types
5 |
6 | import numpy as np
7 |
8 | # Helper function to check if the given object is a pandas DataFrame.
9 | def is_dataframe(obj):
10 | try:
11 | import pandas as pd
12 | return isinstance(obj, pd.DataFrame)
13 | except ImportError:
14 | return False
15 |
16 | # Return a numpy array object that represents `data`.
17 | # If `copy_data` is true, always make a copy; otherwise, copy only if necessary.
18 | #
19 | # If a list is given, convert to a numpy array first, using `dtype`. Otherwise,
20 | # `dtype` is unused.
21 | def convert_to_numpy(data, dtype=None, copy_data=True):
22 | # Sanity check.
23 | if isinstance(data, types.GeneratorType):
24 | raise TypeError(
25 | f'Cannot convert a generator {type(data)} to a buffer: '
26 | 'did you use (... for ... in ...) instead of [...]?')
27 |
28 | data = np.array(data, copy=copy_data, dtype=dtype)
29 | if data.dtype.hasobject:
30 | raise TypeError('Object-type numpy arrays are not supported.')
31 |
32 | return data
33 |
34 | # Helper class to check that various numpy arrays we receive are in the correct
35 | # dimension.
36 | class DimensionChecker():
37 | def __init__(self):
38 | self.dim_info = []
39 | self.dims = collections.defaultdict(dict)
40 |
41 | # Record the dimensions of parameter `data` (with name `name`), where each
42 | # dimension is named using `dim_names`.
43 | #
44 | # If multi-dimensional lists are possible (e.g., X = [[1,2],[3,4]]), the
45 | # caller must first call ensure_buffer() or convert_to_numpy() so that we
46 | # can infer the correct dimension.
47 | #
48 | # Due to broadcast logic, not all dimensions need to be present. E.g., if
49 | # dim_names == ('lines', 'pts'), then `data` can have shape (200,), in which
50 | # case we only record `pts == 200` (`lines` will be absent).
51 | def add(self, name, data, dim_names : tuple, min_dims=1):
52 | assert type(dim_names) == tuple
53 | try:
54 | shape = data.shape
55 | except AttributeError:
56 | shape = (len(data),)
57 |
58 | self.dim_info.append((name, shape))
59 |
60 | assert min_dims <= len(shape) <= len(dim_names)
61 | dim_names = dim_names[-len(shape):]
62 | for dim_name, sz in zip(dim_names, shape):
63 | self.dims[dim_name][name] = sz
64 |
65 | # Verify that the recorded dimensions are consistent.
66 | def verify(self, dim_name, default=None, must=None):
67 | def _raise(msg):
68 | raise ValueError(
69 | msg + ': please check if arguments have correct shapes:' +
70 | str(self.dim_info))
71 |
72 | if dim_name not in self.dims:
73 | if default is None: _raise(f'Missing dimension ({dim_name})')
74 | return default
75 |
76 | s = set(self.dims[dim_name].values())
77 | if len(s) != 1: _raise(f'Inconsistent dimension ({dim_name})')
78 | dim, = s
79 | if must is not None and dim != must:
80 | _raise(f'Dimension ({dim_name}) must be {must}')
81 | return dim
82 |
--------------------------------------------------------------------------------
/src/croquis/display.py:
--------------------------------------------------------------------------------
1 | # Helper class for interfacing with IPython display API.
2 |
3 | import uuid
4 |
5 | import IPython.display
6 | import jinja2
7 |
8 | from . import comm, env_helper
9 |
10 | comm_reopened = False
11 |
12 | # Encapsulates a canvas output, with a unique "canvas ID" (which is our own -
13 | # shouldn't be confused with anything IPython does).
14 | class DisplayObj(object):
15 | # `debug=True` enables these buttons for FE debugging:
16 | #
17 | # * Record: Start recording events and logs into the internal buffer.
18 | # * Stop: Stop recording.
19 | # * Save: Save the recorded events and logs to a file.
20 | # * Load: Load the saved events from a previous run.
21 | # * Replay: Start replaying the events generated by either "Record" or
22 | # "Load".
23 | # * Clear: Clear the internal buffer.
24 | #
25 | # Load/Replay buttons were added to debug the highlight algorithm (i.e.,
26 | # "Why does it not highlight line #3 when the cursor is on top of it?"), but
27 | # the code is very haphazard - there's no guarantee that they may work.
28 | #
29 | # See `EventReplayer` in croquis_fe.js for details.
30 | DEFAULT_CONFIG = {
31 | 'debug': False,
32 | 'reload_fe': False, # Force reload frontend js, for debugging.
33 |
34 | # Where to find our js modules: see env_helper.py and setup.py.
35 | '_js_loader_module':
36 | 'croquis_loader_dev' if env_helper.is_dev()
37 | else 'croquis_fe/croquis_loader',
38 | '_js_main_module':
39 | 'croquis_fe_dev' if env_helper.is_dev()
40 | else 'croquis_fe/croquis_fe',
41 | }
42 |
43 | def __init__(self, **kwargs):
44 | # Create a random "unique" ID.
45 | #
46 | # We add a prefix, because document.querySelector() in js doesn't like
47 | # ID's starting with a digit. (Well, weirdly, jquery seems to work just
48 | # fine with these IDs, so YMMV...)
49 | self.canvas_id = 'v-' + str(uuid.uuid4())
50 |
51 | self.config = {'canvas_id': self.canvas_id}
52 | for key, default_val in self.DEFAULT_CONFIG.items():
53 | self.config[key] = kwargs.pop(key, default_val)
54 | assert not kwargs, 'Unknown options: ' + str(kwargs)
55 |
56 | def register_handler(self, msgtype, callback):
57 | comm.comm_manager.register_handler(self.canvas_id, msgtype, callback)
58 |
59 | def show(self):
60 | # In dev environment, always force reload frontend after kernel restart.
61 | global comm_reopened
62 | if not comm_reopened:
63 | comm_reopened = True
64 | if env_helper.is_dev():
65 | self.config['reload_fe'] = True
66 |
67 | self.config['BE_uuid'] = comm.BE_uuid
68 |
69 | # NOTE: Please keep this in sync with comments on Ctxt (croquis_fe.js).
70 | html = jinja2.Template('''
71 |