diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml
index 6ab3295b..4ebd8e12 100644
--- a/.github/workflows/deploy-docs.yml
+++ b/.github/workflows/deploy-docs.yml
@@ -1,8 +1,5 @@
-# From https://github.com/mitmproxy/pdoc/blob/main/.github/workflows/docs.yml
-# with minor adaptations
-
-name: Deploy docs
-
+# From https://github.com/mhausenblas/mkdocs-deploy-gh-pages
+name: Publish mkDocs via GitHub Pages
# build the documentation whenever there are new commits on main
on:
push:
@@ -12,38 +9,18 @@ on:
# tags:
# - '*'
-# security: restrict permissions for CI jobs.
-permissions:
- contents: read
-
jobs:
- # Build the documentation and upload the static HTML files as an artifact.
build:
+ name: Deploy MkDocs
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
- - uses: actions/setup-python@v4
- with:
- python-version: '3.10'
-
- - run: pip install -e .[build]
- - run: pdoc -o docs-generated/ -t docs/templates --math --docformat=numpy docs/bib/bib.py docs/dev_guide.py ./dapper
+ - name: Checkout main
+ uses: actions/checkout@v2
- - uses: actions/upload-pages-artifact@v1
- with:
- path: docs-generated/
-
- # Deploy the artifact to GitHub pages.
- # This is a separate job so that only actions/deploy-pages has the necessary permissions.
- deploy:
- needs: build
- runs-on: ubuntu-latest
- permissions:
- pages: write
- id-token: write
- environment:
- name: github-pages
- url: ${{ steps.deployment.outputs.page_url }}
- steps:
- - id: deployment
- uses: actions/deploy-pages@v1
+ - name: Deploy docs
+ uses: mhausenblas/mkdocs-deploy-gh-pages@master
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ CONFIG_FILE: mkdocs.yml
+ REQUIREMENTS: requirements.txt
+ # CUSTOM_DOMAIN: optionaldomain.com
diff --git a/.gitignore b/.gitignore
index 18d3ded3..01439c6a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -154,9 +154,6 @@ cython_debug/
# Don't commit results
dpr_data/
-# Don't include local builds of docs
-doc-build
-
# Generated when running QG model
dapper/mods/QG/f90/prms_*\.txt
diff --git a/.markdownlint.yaml b/.markdownlint.yaml
new file mode 100644
index 00000000..285b5c66
--- /dev/null
+++ b/.markdownlint.yaml
@@ -0,0 +1,9 @@
+# markdownlint (js library) config
+MD052: false
+first-line-h1: false
+no-multiple-blanks: false
+line-length: false
+# MD029:
+# style: "ordered"
+# MD007: false
+# "MD007": { "indent": 4 }
diff --git a/README.md b/README.md
index b108eee6..a7b3e66b 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
!
-->
-
+
DAPPER is a set of templates for **benchmarking** the performance of **data assimilation** (DA) methods.
The numerical experiments provide support and guidance for new developments in DA.
@@ -27,7 +27,7 @@ and then estimate that truth given the models and noisy observations.
## Getting started
[Install](#installation), then
-read, run and try to understand `examples/basic_{1,2,3}.py`.
+read, run and try to understand `docs/examples/basic_{1,2,3}.py`.
Some of the examples also exist as Jupyter notebooks, and can be run in the cloud
*without installation* (but requiring Google login): [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](http://colab.research.google.com/github/nansencenter/DAPPER).
This [screencast](https://www.youtube.com/watch?v=YtalK0Zkzvg&t=6475s)
@@ -49,7 +49,7 @@ through a variety of typical [test cases](#test-cases-models) and statistics. It
(b) facilitates comparative studies, thus promoting the
(a) reliability and
(b) relevance of the results.
-For example, the figure below is generated by `examples/basic_3.py`,
+For example, the figure below is generated by `docs/examples/basic_3.py`,
reproduces figure 5.7 of [these lecture notes](http://cerea.enpc.fr/HomePages/bocquet/teaching/assim-mb-en.pdf).
DAPPER is
(c) open source, written in Python, and
@@ -59,10 +59,10 @@ this promotes the
(d) dissemination of the underlying science,
and makes it easy to adapt and extend.
-![Comparative benchmarks with Lorenz-96 plotted as a function of the ensemble size (N)](./docs/imgs/ex3.svg)
+![Comparative benchmarks with Lorenz-96 plotted as a function of the ensemble size (N)](https://github.com/nansencenter/DAPPER/raw/master/docs/images/ex3.svg)
DAPPER demonstrates how to parallelise ensemble forecasts (e.g., the QG model),
-local analyses (e.g., the LETKF), and independent experiments (e.g., `examples/basic_3.py`).
+local analyses (e.g., the LETKF), and independent experiments (e.g., `docs/examples/basic_3.py`).
It includes a battery of diagnostics and statistics,
which all get averaged over subdomains (e.g., "ocean" and "land") and then in time.
Confidence intervals are computed, including correction for auto-correlations,
@@ -72,7 +72,7 @@ which may be paused for further interactive inspection.
In summary, DAPPER is well suited for teaching and fundamental DA research.
Also see its [drawbacks](#similar-projects).
-![EnKF - Lorenz-96](./docs/imgs/ex1.jpg)
+![EnKF - Lorenz-96](https://github.com/nansencenter/DAPPER/raw/master/docs/images/ex1.jpg)
-![NORCE](./docs/imgs/norce-logo.png)
-![NERSC](./docs/imgs/nansen-logo.png)
-
-
-
+![NORCE](https://github.com/nansencenter/DAPPER/raw/master/docs/images/logos/norce-logo.png)
+![NERSC](https://github.com/nansencenter/DAPPER/raw/master/docs/images/logos/nansen-logo.png)
+
+
+
## Publications
diff --git a/dapper/README.md b/dapper/README.md
index ed35297b..e00f5adc 100644
--- a/dapper/README.md
+++ b/dapper/README.md
@@ -1,20 +1,3 @@
-## API reference
-
-Click the links in the navigation menu to view
-the docs for the various modules.
-
-## Installation
-
-See [README/Installation](https://github.com/nansencenter/DAPPER#installation)
-
-## Usage
-
-See [README/Getting-started](https://github.com/nansencenter/DAPPER#getting-started)
-
-##### Examples
-
-See [examples/README](https://github.com/nansencenter/DAPPER/tree/master/examples)
-
##### Adding your own model/method
If you wish to illustrate and run benchmarks with
@@ -22,22 +5,13 @@ your own **model** or **method**, then
- If it is a complex one, you may be better off using DAPPER
merely as *inspiration* (but you can still
- [cite it](https://github.com/nansencenter/DAPPER#getting-started))
+ [cite it](../#getting-started))
rather than trying to squeeze everything into its templates.
- If it is relatively simple, however, you may well want to use DAPPER.
In that case, read this:
- - `dapper.mods`
- - `dapper.da_methods`
+ - [`mods`](mods)
+ - [`da_methods`](da_methods)
Since the generality of DAPPER is
-[limited](https://github.com/nansencenter/DAPPER#similar-projects)
+[limited](../#similar-projects)
it is quite likely you will also need to make changes to the DAPPER code itself.
-
-## Developer guide
-
-If you are making a pull request, please read the [developer guide](dev_guide).
-
-## Bibliography
-
-Click the various citations/references (e.g. `bib.anderson2010non`)
-to access the [bibliography](bib).
diff --git a/dapper/__init__.py b/dapper/__init__.py
index 11a65160..80401d8c 100644
--- a/dapper/__init__.py
+++ b/dapper/__init__.py
@@ -1,7 +1,7 @@
"""Root package of **DAPPER**
(Data Assimilation with Python: a Package for Experimental Research)
-.. include:: ./README.md
+--8<-- "dapper/README.md"
"""
__version__ = "1.7.0"
diff --git a/dapper/da_methods/README.md b/dapper/da_methods/README.md
index 85b619a8..20bb9755 100644
--- a/dapper/da_methods/README.md
+++ b/dapper/da_methods/README.md
@@ -1,6 +1,4 @@
-Also see the section on
-[DA Methods](https://github.com/nansencenter/DAPPER#DA-Methods)
-in the main README
+Also see [this section on DA Methods](../../#da-methods)
for an overview of the methods included with DAPPER.
## Defining your own method
@@ -8,7 +6,7 @@ for an overview of the methods included with DAPPER.
Follow the example of one of the methods within one of the
sub-directories/packages.
The simplest example is perhaps
-`dapper.da_methods.ensemble.EnKF`.
+[`da_methods.ensemble.EnKF`][].
## General advice for programming/debugging scientific experiments
diff --git a/dapper/da_methods/__init__.py b/dapper/da_methods/__init__.py
index 3fb46c23..b6b3bd60 100644
--- a/dapper/da_methods/__init__.py
+++ b/dapper/da_methods/__init__.py
@@ -1,6 +1,6 @@
"""Contains the data assimilation methods included with DAPPER.
-.. include:: ./README.md
+--8<-- "dapper/da_methods/README.md"
"""
from pathlib import Path
@@ -16,7 +16,7 @@ def da_method(*default_dataclasses):
The decorated classes are defined like a `dataclass`,
but are decorated by `@da_method()` instead of `@dataclass`.
- .. note::
+ !!! note
The classes must define a method called `assimilate`.
This method gets slightly enhanced by this wrapper which provides:
@@ -25,7 +25,8 @@ def da_method(*default_dataclasses):
- Duration timing
- Progressbar naming magic.
- Example:
+ Examples
+ --------
>>> @da_method()
... class Sleeper():
... "Do nothing."
@@ -43,7 +44,6 @@ def da_method(*default_dataclasses):
which enables defining default parameters which can be inherited,
similar to subclassing.
- Example:
>>> class ens_defaults:
... infl : float = 1.0
... rot : bool = False
@@ -56,13 +56,13 @@ def da_method(*default_dataclasses):
... def assimilate(self, HMM, xx, yy):
... ...
- .. note::
+ !!! note
Apart from what's listed in the above `Note`, there is nothing special to the
resulting `xp`. That is, just like any Python object, it can serve as a data
container, and you can write any number of attributes to it (at creation-time,
or later). For example, you can set attributes that are not used by the
`assimilate` method, but are instead used to customize other aspects of the
- experiments (see `dapper.xp_launch.run_experiment`).
+ experiments (see [`xp_launch.run_experiment`][]).
"""
import dataclasses
import functools
diff --git a/dapper/da_methods/baseline.py b/dapper/da_methods/baseline.py
index 64cb0b96..87903539 100644
--- a/dapper/da_methods/baseline.py
+++ b/dapper/da_methods/baseline.py
@@ -1,6 +1,6 @@
"""Unsophisticated" but robust (widely applicable) DA methods.
-Many are based on `bib.raanes2016thesis`.
+Many are based on [raanes2016thesis][].
"""
from typing import Callable, Optional
@@ -146,10 +146,15 @@ def fit_sigmoid(Sb, L, kb):
The "normalized" sigmoid, `S1`, is symmetric around 0, and `S1(-∞)=0` and `S1(∞)=1`.
- The sigmoid `S(k) = S1(a*(k-kb) + b)` is fitted (see docs/snippets/sigmoid.jpg) with
+ The sigmoid `S(k) = S1(a*(k-kb) + b)` is fitted with
- `a` corresponding to a given corr. length `L`.
- `b` to match values of `S(kb)` and `Sb`
+
+
"""
def sigmoid(k):
diff --git a/dapper/da_methods/ensemble.py b/dapper/da_methods/ensemble.py
index aa9b40b9..96507316 100644
--- a/dapper/da_methods/ensemble.py
+++ b/dapper/da_methods/ensemble.py
@@ -28,7 +28,7 @@ class ens_method:
class EnKF:
"""The ensemble Kalman filter.
- Refs: `bib.evensen2009ensemble`.
+ Refs: [evensen2009ensemble][].
"""
upd_a: str
@@ -67,8 +67,8 @@ def EnKF_analysis(E, Eo, hnoise, y, upd_a, stats=None, ko=None):
This implementation includes several flavours and forms,
specified by `upd_a`.
- Main references: `bib.sakov2008deterministic`,
- `bib.sakov2008implications`, `bib.hoteit2015mitigating`
+ Main references: [sakov2008deterministic][],
+ [sakov2008implications][], [hoteit2015mitigating][]
"""
R = hnoise.C # Obs noise cov
N, Nx = E.shape # Dimensionality
@@ -113,7 +113,7 @@ def EnKF_analysis(E, Eo, hnoise, y, upd_a, stats=None, ko=None):
d = pad0(s**2, N) + N1
Pw = (V * d ** (-1.0)) @ V.T
T = (V * d ** (-0.5)) @ V.T * sqrt(N1)
- # docs/snippets/trHK.jpg
+ # docs/images/snippets/trHK.jpg
trHK = np.sum((s**2 + N1) ** (-1.0) * s**2)
elif "sS" in upd_a:
# Same as 'svd', but with slightly different notation
@@ -123,7 +123,7 @@ def EnKF_analysis(E, Eo, hnoise, y, upd_a, stats=None, ko=None):
d = pad0(s**2, N) + 1
Pw = (V * d ** (-1.0)) @ V.T / N1 # = G/(N1)
T = (V * d ** (-0.5)) @ V.T
- # docs/snippets/trHK.jpg
+ # docs/images/snippets/trHK.jpg
trHK = np.sum((s**2 + 1) ** (-1.0) * s**2)
else: # 'eig' in upd_a:
# Implementation using eig. val. decomp.
@@ -261,7 +261,7 @@ def post_process(E, infl, rot):
def add_noise(E, dt, noise, method):
"""Treatment of additive noise for ensembles.
- Refs: `bib.raanes2014ext`
+ Refs: [raanes2014ext][]
"""
if noise.C == 0:
return E
@@ -354,7 +354,7 @@ def sqrt_core():
class EnKS:
"""The ensemble Kalman smoother.
- Refs: `bib.evensen2009ensemble`
+ Refs: [evensen2009ensemble][]
The only difference to the EnKF
is the management of the lag and the reshapings.
@@ -414,7 +414,7 @@ def assimilate(self, HMM, xx, yy):
class EnRTS:
"""EnRTS (Rauch-Tung-Striebel) smoother.
- Refs: `bib.raanes2016thesis`
+ Refs: [raanes2016thesis][]
"""
upd_a: str
@@ -484,7 +484,7 @@ def serial_inds(upd_a, y, cvR, A):
class SL_EAKF:
"""Serial, covariance-localized EAKF.
- Refs: `bib.karspeck2007experimental`.
+ Refs: [karspeck2007experimental][].
In contrast with LETKF, this iterates over the observations rather
than over the state (batches).
@@ -614,10 +614,10 @@ def local_analysis(ii):
class LETKF:
"""Same as EnKF (Sqrt), but with localization.
- Refs: `bib.hunt2007efficient`.
+ Refs: [hunt2007efficient][].
- NB: Multiproc. yields slow-down for `dapper.mods.Lorenz96`,
- even with `batch_size=(1,)`. But for `dapper.mods.QG`
+ NB: Multiproc. yields slow-down for [`mods.Lorenz96`][],
+ even with `batch_size=(1,)`. But for [`mods.QG`][]
(`batch_size=(2,2)` or less) it is quicker.
NB: If `len(ii)` is small, analysis may be slowed-down with '-N' infl.
@@ -778,7 +778,7 @@ def hyperprior_coeffs(s, N, xN=1, g=0):
- Reason 1: mode correction.
These parameters bridge the Jeffreys (`xN=1`) and Dirac (`xN=Inf`) hyperpriors
- for the prior covariance, B, as discussed in `bib.bocquet2015expanding`.
+ for the prior covariance, B, as discussed in [bocquet2015expanding][].
Indeed, mode correction becomes necessary when $$ R \rightarrow \infty $$
because then there should be no ensemble update (and also no inflation!).
More specifically, the mode of `l1`'s should be adjusted towards 1
@@ -790,7 +790,7 @@ def hyperprior_coeffs(s, N, xN=1, g=0):
- Reason 2: Boosting the inflation prior's certainty from N to xN*N.
The aim is to take advantage of the fact that the ensemble may not
have quite as much sampling error as a fully stochastic sample,
- as illustrated in section 2.1 of `bib.raanes2019adaptive`.
+ as illustrated in section 2.1 of [raanes2019adaptive][].
- Its damping effect is similar to work done by J. Anderson.
@@ -809,7 +809,7 @@ def hyperprior_coeffs(s, N, xN=1, g=0):
eN = (N + 1) / N
cL = (N + g) / N1
- # Mode correction (almost) as in eqn 36 of `bib.bocquet2015expanding`
+ # Mode correction (almost) as in eqn 36 of [bocquet2015expanding][]
prior_mode = eN / cL # Mode of l1 (before correction)
diagonal = pad0(s**2, N) + N1 # diag of Y@R.inv@Y + N1*I
# (Hessian of J)
@@ -834,7 +834,7 @@ def zeta_a(eN, cL, w):
Returns `zeta_a = (N-1)/pre-inflation^2`.
Using this inside an iterative minimization as in the
- `dapper.da_methods.variational.iEnKS` effectively blends
+ [`da_methods.variational.iEnKS`][] effectively blends
the distinction between the primal and dual EnKF-N.
"""
N = len(w)
@@ -847,12 +847,12 @@ def zeta_a(eN, cL, w):
class EnKF_N:
"""Finite-size EnKF (EnKF-N).
- Refs: `bib.bocquet2011ensemble`, `bib.bocquet2015expanding`
+ Refs: [bocquet2011ensemble][], [bocquet2015expanding][]
This implementation is pedagogical, prioritizing the "dual" form.
In consequence, the efficiency of the "primal" form suffers a bit.
The primal form is included for completeness and to demonstrate equivalence.
- In `dapper.da_methods.variational.iEnKS`, however,
+ In [`da_methods.variational.iEnKS`][], however,
the primal form is preferred because it
already does optimization for w (as treatment for nonlinear models).
@@ -985,7 +985,7 @@ def nvrs(w):
# l1 = 1.0
# Explicitly inflate prior
- # => formulae look different from `bib.bocquet2015expanding`.
+ # => formulae look different from [bocquet2015expanding][].
A *= l1
Y *= l1
@@ -1000,7 +1000,7 @@ def nvrs(w):
else:
# Also include angular-radial co-dependence.
# Note: denominator not squared coz
- # unlike `bib.bocquet2015expanding` we have inflated Y.
+ # unlike [bocquet2015expanding][] we have inflated Y.
Hw = (
Y @ R.inv @ Y.T / N1
+ eye(N)
diff --git a/dapper/da_methods/other.py b/dapper/da_methods/other.py
index 95d44512..f13a997c 100644
--- a/dapper/da_methods/other.py
+++ b/dapper/da_methods/other.py
@@ -15,7 +15,7 @@
class RHF:
"""Rank histogram filter.
- Refs: `bib.anderson2010non`.
+ Refs: [anderson2010non][].
Quick & dirty implementation without attention to (de)tails.
"""
@@ -86,9 +86,9 @@ def assimilate(self, HMM, xx, yy):
class LNETF:
"""The Nonlinear-Ensemble-Transform-Filter (localized).
- Refs: `bib.wiljes2016second`, `bib.todter2015second`.
+ Refs: [wiljes2016second][], [todter2015second][].
- It is (supposedly) a deterministic upgrade of the NLEAF of `bib.lei2011moment`.
+ It is (supposedly) a deterministic upgrade of the NLEAF of [lei2011moment][].
"""
N: int
diff --git a/dapper/da_methods/particle.py b/dapper/da_methods/particle.py
index b3f1fbbd..7e929329 100644
--- a/dapper/da_methods/particle.py
+++ b/dapper/da_methods/particle.py
@@ -23,7 +23,7 @@ class particle_method:
class PartFilt:
r"""Particle filter ≡ Sequential importance (re)sampling SIS (SIR).
- Refs: `bib.wikle2007bayesian`, `bib.van2009particle`, `bib.chen2003bayesian`
+ Refs: [wikle2007bayesian][], [van2009particle][], [chen2003bayesian][]
This is the bootstrap version: the proposal density is just
@@ -37,7 +37,7 @@ class PartFilt:
- qroot: "Inflate" (anneal) the proposal noise kernels
by this root to increase diversity.
The weights are updated to maintain un-biased-ness.
- See `bib.chen2003bayesian`, section VI-M.2
+ See [chen2003bayesian][], section VI-M.2
"""
N: int
@@ -92,11 +92,10 @@ def assimilate(self, HMM, xx, yy):
class OptPF:
"""'Optimal proposal' particle filter, also known as 'Implicit particle filter'.
- Ref: `bib.bocquet2010beyond`.
+ Ref: [bocquet2010beyond][].
- .. note:: Regularization (`Qs`) is here added BEFORE Bayes' rule.
- If `Qs==0`: OptPF should be equal to
- the bootstrap filter `PartFilt`.
+ !!! note Regularization (`Qs`) is here added BEFORE Bayes' rule.
+ If `Qs==0`: OptPF should be equal to the bootstrap filter `PartFilt`.
"""
N: int
@@ -521,7 +520,7 @@ def mask_unique_of_sorted(idx):
def auto_bandw(N, M):
"""Optimal bandwidth (not bandwidth^2), as per Scott's rule-of-thumb.
- Refs: `bib.doucet2001sequential` section 12.2.2, [Wik17]_ section "Rule_of_thumb"
+ Refs: [doucet2001sequential][] section 12.2.2, [Wik17]_ section "Rule_of_thumb"
"""
return N ** (-1 / (M + 4))
@@ -535,7 +534,7 @@ def regularize(C12, E, idx, no_uniq_jitter):
(so-named because Dirac-deltas are approximated Gaussian kernels),
which controls the strength of the jitter.
This causes a bias. But, as N-->∞, the reg. bandwidth-->0, i.e. bias-->0.
- Ref: `bib.doucet2001sequential`, section 12.2.2.
+ Ref: [doucet2001sequential][], section 12.2.2.
"""
# Select
E = E[idx]
@@ -555,7 +554,7 @@ def regularize(C12, E, idx, no_uniq_jitter):
def resample(w, kind="Systematic", N=None, wroot=1.0):
"""Multinomial resampling.
- Refs: `bib.doucet2009tutorial`, `bib.van2009particle`, `bib.liu2001theoretical`.
+ Refs: [doucet2009tutorial][], [van2009particle][], [liu2001theoretical][].
- kind: 'Systematic', 'Residual' or 'Stochastic'.
'Stochastic' corresponds to `rng.choice` or `rng.multinomial`.
@@ -564,7 +563,7 @@ def resample(w, kind="Systematic", N=None, wroot=1.0):
Among the three, 'Systematic' is fastest, introduces the least noise,
and brings continuity benefits for localized particle filters,
and is therefore generally prefered.
- Example: see docs/snippets/ex_resample.py.
+ Example: see `docs/images/snippets/ex_resample.py`.
- N can be different from len(w)
(e.g. in case some particles have been elimintated).
@@ -572,7 +571,7 @@ def resample(w, kind="Systematic", N=None, wroot=1.0):
- wroot: Adjust weights before resampling by this root to
promote particle diversity and mitigate thinning.
The outcomes of the resampling are then weighted to maintain un-biased-ness.
- Ref: `bib.liu2001theoretical`, section 3.1
+ Ref: [liu2001theoretical][], section 3.1
Note: (a) resampling methods are beneficial because they discard
low-weight ("doomed") particles and reduce the variance of the weights.
diff --git a/dapper/da_methods/variational.py b/dapper/da_methods/variational.py
index 981f1123..9e344d17 100644
--- a/dapper/da_methods/variational.py
+++ b/dapper/da_methods/variational.py
@@ -28,10 +28,10 @@ class var_method:
class iEnKS:
"""Iterative EnKS.
- Special cases: EnRML, ES-MDA, iEnKF, EnKF `bib.raanes2019revising`.
+ Special cases: EnRML, ES-MDA, iEnKF, EnKF [raanes2019revising][].
- As in `bib.bocquet2014iterative`, optimization uses Gauss-Newton.
- See `bib.bocquet2012combining` for Levenberg-Marquardt.
+ As in [bocquet2014iterative][], optimization uses Gauss-Newton.
+ See [bocquet2012combining][] for Levenberg-Marquardt.
If MDA=True, then there's not really any optimization,
but rather Gaussian annealing.
@@ -41,13 +41,13 @@ class iEnKS:
- "Sqrt" : as in ETKF , using a deterministic matrix square root transform.
- "PertObs": as in EnRML , using stochastic, perturbed-observations.
- - "Order1" : as in DEnKF of `bib.sakov2008deterministic`.
+ - "Order1" : as in DEnKF of [sakov2008deterministic][].
Lag:
Length of the DA window (DAW, multiples of dko, i.e. cycles).
- - Lag=1 (default) => iterative "filter" iEnKF `bib.sakov2012iterative`.
- - Lag=0 => maximum-likelihood filter `bib.zupanski2005maximum`.
+ - Lag=1 (default) => iterative "filter" iEnKF [sakov2012iterative][].
+ - Lag=0 => maximum-likelihood filter [zupanski2005maximum][].
Shift : How far (in cycles) to slide the DAW.
Fixed at 1 for code simplicity.
@@ -60,7 +60,7 @@ class iEnKS:
Recommended: 1e-5.
MDA : Use iterations of the "multiple data assimlation" type.
- Ref `bib.emerick2012history`
+ Ref [emerick2012history][]
bundle: Use finite-diff. linearization instead of of least-squares regression.
Makes the iEnKS very much alike the iterative, extended KF (IEKS).
@@ -70,8 +70,8 @@ class iEnKS:
Total number of model simulations (of duration dto): N * (nIter*Lag + 1).
(due to boundary cases: only asymptotically valid)
- Refs: `bib.bocquet2012combining`, `bib.bocquet2013joint`,
- `bib.bocquet2014iterative`.
+ Refs: [bocquet2012combining][], [bocquet2013joint][],
+ [bocquet2014iterative][].
"""
upd_a: str
@@ -264,7 +264,7 @@ def Cowp(expo):
# (i) avoid re-running the model and
# (ii) reproduce EnKF in case nIter==1.
final_increment = (dw + T - T_old) @ Xf
- # See docs/snippets/iEnKS_Ea.jpg.
+ # See docs/images/snippets/iEnKS_Ea.jpg.
stats.assess(k, ko, "a", E=E + final_increment)
stats.iters[ko] = iteration + 1
if xN:
diff --git a/dapper/mods/DoublePendulum/__init__.py b/dapper/mods/DoublePendulum/__init__.py
index 7d124dcf..723f4bfa 100644
--- a/dapper/mods/DoublePendulum/__init__.py
+++ b/dapper/mods/DoublePendulum/__init__.py
@@ -27,6 +27,7 @@
@modelling.ens_compatible
def dxdt(x):
+ """Evolution equation (coupled ODEs) specifying the dynamics."""
th1, w1, th2, w2 = x
dydx = np.zeros_like(x)
diff --git a/dapper/mods/KS/bocquet2019.py b/dapper/mods/KS/bocquet2019.py
index 8a285ca5..936ee162 100644
--- a/dapper/mods/KS/bocquet2019.py
+++ b/dapper/mods/KS/bocquet2019.py
@@ -1,4 +1,4 @@
-"""Settings as in `bib.bocquet2019consistency`."""
+"""Settings as in [bocquet2019consistency][]."""
import numpy as np
diff --git a/dapper/mods/LA/__init__.py b/dapper/mods/LA/__init__.py
index 5d115d9f..0432cf3e 100644
--- a/dapper/mods/LA/__init__.py
+++ b/dapper/mods/LA/__init__.py
@@ -76,7 +76,8 @@ def sinusoidal_sample(Nx, k, N):
but serves to avoid the initial transitory regime
if the model is dissipative(, and more ?).
- Example:
+ Examples
+ --------
>>> E = sinusoidal_sample(100, 4, 5)
>>> plt.plot(E.T) # doctest: +SKIP
"""
diff --git a/dapper/mods/LA/evensen2009.py b/dapper/mods/LA/evensen2009.py
index 5dd4a854..26de4e63 100644
--- a/dapper/mods/LA/evensen2009.py
+++ b/dapper/mods/LA/evensen2009.py
@@ -1,12 +1,12 @@
-"""A mix of `bib.evensen2009ensemble` and `bib.sakov2008implications`.
+"""A mix of [evensen2009ensemble][] and [sakov2008implications][].
-.. note::
+!!! note
Since there is no noise, and the system is stable,
the rmse's from this HMM go to zero as `T` goes to infinity.
Thus, benchmarks largely depend on the initial error,
and so these absolute rmse values are not so useful
for quantatative evaluation of DA methods.
- For that purpose, see `dapper.mods.LA.raanes2015` instead.
+ For that purpose, see [`mods.LA.raanes2015`][] instead.
"""
import numpy as np
diff --git a/dapper/mods/LA/raanes2015.py b/dapper/mods/LA/raanes2015.py
index b897cd03..b50835b5 100644
--- a/dapper/mods/LA/raanes2015.py
+++ b/dapper/mods/LA/raanes2015.py
@@ -1,4 +1,4 @@
-"""Reproduce results from Fig. 2 of `bib.raanes2014ext`."""
+"""Reproduce results from Fig. 2 of [raanes2014ext][]."""
import numpy as np
diff --git a/dapper/mods/Lorenz05/__init__.py b/dapper/mods/Lorenz05/__init__.py
index f6758240..a85e947a 100644
--- a/dapper/mods/Lorenz05/__init__.py
+++ b/dapper/mods/Lorenz05/__init__.py
@@ -1,8 +1,8 @@
"""A multi-scale, smooth version of the classic Lorenz-96.
-This is an implementation of "Model III" of `bib.lorenz2005designing`.
+This is an implementation of "Model III" of [lorenz2005designing][].
-Similar to `dapper.mods.LorenzUV` this model is designed
+Similar to [`mods.LorenzUV`][] this model is designed
to contain two different scales. However, in "Model III"
the two scales are not kept separate, but superimposed,
and the large scale variables are (adjustably) spatially smooth.
@@ -111,7 +111,7 @@ def step(self, E, t, dt):
def summation_kernel(width):
- """Prepare computation of the modified sum in `bib.lorenz2005designing`.
+ """Prepare computation of the modified sum in [lorenz2005designing][].
Note: This gets repeatedly called, but actually the input is only ever
`width = K` or `2*J`, so we should really cache or pre-compute.
@@ -131,7 +131,7 @@ def boxcar(x, n, method="direct"):
For symmetry, if `n` is pair, the actual number of elements used is `n+1`,
and the outer elements weighted by `0.5` to compensate for the `+1`.
- This is the modified sum of `bib.lorenz2005designing`, used e.g. in eqn. 9.
+ This is the modified sum of [lorenz2005designing][], used e.g. in eqn. 9.
For intuition: this type of summation (and the associated Sigma prime notation)
may also be found for the "Trapezoidal rule" and in the inverse DFT used in
spectral methods on a periodic domain.
@@ -151,7 +151,8 @@ def boxcar(x, n, method="direct"):
method is a little slower again. If `K` or `J` is increased, then the "fft"
method becomes the fastest.
- Examples:
+ Examples
+ --------
>>> x = np.array([0, 1, 2], dtype=float)
>>> np.allclose(boxcar(x, 1), x)
True
@@ -216,7 +217,7 @@ def shift(x, k):
def prodsum_self(x, k):
- """Compute `prodsum(x, x, k)` efficiently: eqn 10 of `bib.lorenz2005designing`."""
+ """Compute `prodsum(x, x, k)` efficiently: eqn 10 of [lorenz2005designing][]."""
W = boxcar(x, k)
WW = shift(W, -2 * k) * shift(W, -k)
WX = shift(W, -k) * shift(x, k)
diff --git a/dapper/mods/Lorenz63/anderson2010rhf.py b/dapper/mods/Lorenz63/anderson2010rhf.py
index dc05a8af..036cab44 100644
--- a/dapper/mods/Lorenz63/anderson2010rhf.py
+++ b/dapper/mods/Lorenz63/anderson2010rhf.py
@@ -1,4 +1,4 @@
-"""Settings from `bib.anderson2010non`."""
+"""Settings from [anderson2010non][]."""
import numpy as np
diff --git a/dapper/mods/Lorenz63/bocquet2012.py b/dapper/mods/Lorenz63/bocquet2012.py
index 23c5f06e..a2641ee2 100644
--- a/dapper/mods/Lorenz63/bocquet2012.py
+++ b/dapper/mods/Lorenz63/bocquet2012.py
@@ -1,4 +1,4 @@
-"""Reproduce results from Fig 11 of `bib.bocquet2012combining`."""
+"""Reproduce results from Fig 11 of [bocquet2012combining][]."""
import numpy as np
diff --git a/dapper/mods/Lorenz63/mandel2016.py b/dapper/mods/Lorenz63/mandel2016.py
index d9d74ddf..ce08a444 100644
--- a/dapper/mods/Lorenz63/mandel2016.py
+++ b/dapper/mods/Lorenz63/mandel2016.py
@@ -1,4 +1,4 @@
-"""Settings from `bib.mandel2016hybrid`."""
+"""Settings from [mandel2016hybrid][]."""
import dapper.mods as modelling
from dapper.mods.Lorenz63.sakov2012 import HMM as _HMM
diff --git a/dapper/mods/Lorenz63/sakov2012.py b/dapper/mods/Lorenz63/sakov2012.py
index 7dc1c699..11d8488d 100644
--- a/dapper/mods/Lorenz63/sakov2012.py
+++ b/dapper/mods/Lorenz63/sakov2012.py
@@ -1,4 +1,4 @@
-"""Reproduce results from Table 1 `bib.sakov2012iterative`."""
+"""Reproduce results from Table 1 [sakov2012iterative][]."""
import numpy as np
diff --git a/dapper/mods/Lorenz63/wiljes2017.py b/dapper/mods/Lorenz63/wiljes2017.py
index d6def422..a1471de9 100644
--- a/dapper/mods/Lorenz63/wiljes2017.py
+++ b/dapper/mods/Lorenz63/wiljes2017.py
@@ -1,4 +1,4 @@
-"""Settings from `bib.wiljes2016second`."""
+"""Settings from [wiljes2016second][]."""
import numpy as np
diff --git a/dapper/mods/Lorenz84/__init__.py b/dapper/mods/Lorenz84/__init__.py
index a069780f..4f27bf39 100644
--- a/dapper/mods/Lorenz84/__init__.py
+++ b/dapper/mods/Lorenz84/__init__.py
@@ -1,6 +1,6 @@
"""A chaotic system of size 3, like Lorenz-63, but with +complex geometry.
-Refs: `bib.lorenz1984irregularity`, `bib.lorenz2005look`
+Refs: [lorenz1984irregularity][], [lorenz2005look][]
"""
import numpy as np
diff --git a/dapper/mods/Lorenz84/harder.py b/dapper/mods/Lorenz84/harder.py
index db174cc2..3d59ef3d 100644
--- a/dapper/mods/Lorenz84/harder.py
+++ b/dapper/mods/Lorenz84/harder.py
@@ -1,4 +1,4 @@
-"""Harder settings than in `dapper.mods.Lorenz84.pajonk2012`.
+"""Harder settings than in [`mods.Lorenz84.pajonk2012`][].
This was adjudged by noting that with their settings,
the average val. of `trHK` is 0.013.
diff --git a/dapper/mods/Lorenz84/pajonk2012.py b/dapper/mods/Lorenz84/pajonk2012.py
index 35121dca..7b03b826 100644
--- a/dapper/mods/Lorenz84/pajonk2012.py
+++ b/dapper/mods/Lorenz84/pajonk2012.py
@@ -1,4 +1,4 @@
-"""Settings from `bib.pajonk2012deterministic`.
+"""Settings from [pajonk2012deterministic][].
There is nothing to reproduce from the paper as there are no
statistically converged numbers.
diff --git a/dapper/mods/Lorenz96/__init__.py b/dapper/mods/Lorenz96/__init__.py
index d0af3373..d80ef9d8 100644
--- a/dapper/mods/Lorenz96/__init__.py
+++ b/dapper/mods/Lorenz96/__init__.py
@@ -1,6 +1,6 @@
"""A 1D emulator of chaotic atmospheric behaviour.
-`bib.lorenz1996predictability`
+[lorenz1996predictability][]
For a short introduction, see
diff --git a/dapper/mods/Lorenz96/anderson2009.py b/dapper/mods/Lorenz96/anderson2009.py
index e5bef2e7..8d8ea7e0 100644
--- a/dapper/mods/Lorenz96/anderson2009.py
+++ b/dapper/mods/Lorenz96/anderson2009.py
@@ -1,4 +1,4 @@
-"""A land-ocean setup from `bib.anderson2009spatially`."""
+"""A land-ocean setup from [anderson2009spatially][]."""
import numpy as np
diff --git a/dapper/mods/Lorenz96/bocquet2010.py b/dapper/mods/Lorenz96/bocquet2010.py
index 70b5160d..d942e92b 100644
--- a/dapper/mods/Lorenz96/bocquet2010.py
+++ b/dapper/mods/Lorenz96/bocquet2010.py
@@ -1,4 +1,4 @@
-"""From Fig. 1 of `bib.bocquet2010beyond`."""
+"""From Fig. 1 of [bocquet2010beyond][]."""
import numpy as np
diff --git a/dapper/mods/Lorenz96/bocquet2010_m40.py b/dapper/mods/Lorenz96/bocquet2010_m40.py
index 9727bb00..9909be9b 100644
--- a/dapper/mods/Lorenz96/bocquet2010_m40.py
+++ b/dapper/mods/Lorenz96/bocquet2010_m40.py
@@ -1,4 +1,4 @@
-"""From `bib.bocquet2010beyond` (again), but `ndim=40` (i.e. Fig. 5 of paper)."""
+"""From [bocquet2010beyond][] (again), but `ndim=40` (i.e. Fig. 5 of paper)."""
import numpy as np
diff --git a/dapper/mods/Lorenz96/bocquet2015loc.py b/dapper/mods/Lorenz96/bocquet2015loc.py
index fed3abd6..866d773d 100644
--- a/dapper/mods/Lorenz96/bocquet2015loc.py
+++ b/dapper/mods/Lorenz96/bocquet2015loc.py
@@ -1,4 +1,4 @@
-"""Settings as in `bib.bocquet2016localization`."""
+"""Settings as in [bocquet2016localization][]."""
import numpy as np
diff --git a/dapper/mods/Lorenz96/frei2013bridging.py b/dapper/mods/Lorenz96/frei2013bridging.py
index ddc5d1a4..f4c374f6 100644
--- a/dapper/mods/Lorenz96/frei2013bridging.py
+++ b/dapper/mods/Lorenz96/frei2013bridging.py
@@ -1,8 +1,8 @@
-"""Settings as in `bib.frei2013bridging`.
+"""Settings as in [frei2013bridging][].
They also cite its use in the following:
-`bib.bengtsson2003toward`, `bib.lei2011moment`, `bib.frei2013mixture`.
+[bengtsson2003toward][], [lei2011moment][], [frei2013mixture][].
"""
import numpy as np
diff --git a/dapper/mods/Lorenz96/hoteit2015.py b/dapper/mods/Lorenz96/hoteit2015.py
index 4fa0ac54..152b5084 100644
--- a/dapper/mods/Lorenz96/hoteit2015.py
+++ b/dapper/mods/Lorenz96/hoteit2015.py
@@ -1,4 +1,4 @@
-"""Reproduce results from `bib.hoteit2015mitigating`."""
+"""Reproduce results from [hoteit2015mitigating][]."""
from dapper.mods.Lorenz96.sakov2008 import HMM as _HMM
diff --git a/dapper/mods/Lorenz96/miyoshi2011.py b/dapper/mods/Lorenz96/miyoshi2011.py
index 2c6763b6..ce41a7c6 100644
--- a/dapper/mods/Lorenz96/miyoshi2011.py
+++ b/dapper/mods/Lorenz96/miyoshi2011.py
@@ -1,6 +1,6 @@
"""A land-ocean setup.
-Refs: `bib.miyoshi2011gaussian`, which was inspired by `bib.lorenz1998optimal`.
+Refs: [miyoshi2011gaussian][], which was inspired by [lorenz1998optimal][].
"""
import numpy as np
diff --git a/dapper/mods/Lorenz96/pinheiro2019.py b/dapper/mods/Lorenz96/pinheiro2019.py
index 55074c4b..c8c924d0 100644
--- a/dapper/mods/Lorenz96/pinheiro2019.py
+++ b/dapper/mods/Lorenz96/pinheiro2019.py
@@ -1,4 +1,4 @@
-"""Settings from `bib.pinheiro2019efficient`."""
+"""Settings from [pinheiro2019efficient][]."""
import dapper.mods as modelling
import dapper.mods.Lorenz96 as model
diff --git a/dapper/mods/Lorenz96/raanes2016.py b/dapper/mods/Lorenz96/raanes2016.py
index 427bcb06..95b185db 100644
--- a/dapper/mods/Lorenz96/raanes2016.py
+++ b/dapper/mods/Lorenz96/raanes2016.py
@@ -1,4 +1,4 @@
-"""Reproduce `bib.raanes2015rts`."""
+"""Reproduce [raanes2015rts][]."""
import dapper.mods as modelling
from dapper.mods.Lorenz96.sakov2008 import HMM as _HMM
diff --git a/dapper/mods/Lorenz96/sakov2008.py b/dapper/mods/Lorenz96/sakov2008.py
index f28f6373..58f3c795 100644
--- a/dapper/mods/Lorenz96/sakov2008.py
+++ b/dapper/mods/Lorenz96/sakov2008.py
@@ -1,9 +1,9 @@
-"""Settings as in `bib.sakov2008deterministic`.
+"""Settings as in [sakov2008deterministic][].
This HMM is used (with small variations) in many DA papers, for example
-`bib.bocquet2011ensemble`, `bib.sakov2012iterative`,
-`bib.bocquet2015expanding`, `bib.bocquet2013joint`.
+[bocquet2011ensemble][], [sakov2012iterative][],
+[bocquet2015expanding][], [bocquet2013joint][].
"""
import numpy as np
diff --git a/dapper/mods/Lorenz96/todter2015.py b/dapper/mods/Lorenz96/todter2015.py
index e3bc35f5..522069fa 100644
--- a/dapper/mods/Lorenz96/todter2015.py
+++ b/dapper/mods/Lorenz96/todter2015.py
@@ -1,4 +1,4 @@
-"""Concerns figure 4 of `bib.todter2015second`."""
+"""Concerns figure 4 of [todter2015second][]."""
import numpy as np
diff --git a/dapper/mods/Lorenz96/todter2015_G.py b/dapper/mods/Lorenz96/todter2015_G.py
index 6186be65..e35bae8b 100644
--- a/dapper/mods/Lorenz96/todter2015_G.py
+++ b/dapper/mods/Lorenz96/todter2015_G.py
@@ -1,4 +1,4 @@
-"""From `dapper.mods.Lorenz96.todter2015` again, but with Gaussian likelihood."""
+"""From [`mods.Lorenz96.todter2015`][] again, but with Gaussian likelihood."""
import dapper.mods as modelling
from dapper.mods.Lorenz96.todter2015 import HMM as _HMM
diff --git a/dapper/mods/Lorenz96s/__init__.py b/dapper/mods/Lorenz96s/__init__.py
index 89d81db3..5351b45c 100644
--- a/dapper/mods/Lorenz96s/__init__.py
+++ b/dapper/mods/Lorenz96s/__init__.py
@@ -1,6 +1,6 @@
"""A perfect-random version of Lorenz-96.
-Used by `bib.grudzien2020numerical` to study the precision of
+Used by [grudzien2020numerical][] to study the precision of
stochastic integration schemes.
Both the model and truth are to be integrated by the same *random* model (with almost
@@ -11,7 +11,7 @@
The truth twin should be generated by the order 2.0 Taylor scheme below,
for the accuracy with respect to convergence in the strong sense.
-See `bib.grudzien2020numerical` for a full discussion of benchmarks on this
+See [grudzien2020numerical][] for a full discussion of benchmarks on this
model and statistically robust configurations.
This study uses no multiplicative inflation / localization or other
@@ -19,7 +19,7 @@
observation EnKF as a simple estimator to study the asymptotic filtering
statistics under different model scenarios.
-The purpose of the study in `bib.grudzien2020numerical` was to explore the relationships
+The purpose of the study in [grudzien2020numerical][] was to explore the relationships
between:
- numerical discretization error in truth twins;
@@ -38,7 +38,7 @@
thus corresponds to a wider variance of the relizations of the diffeomorphsims
that generate the model / truth twin between observation times.
-It is demonstrated by `bib.grudzien2020numerical` that the model error due to
+It is demonstrated by [grudzien2020numerical][] that the model error due to
discretization of the SDE equations of motion is most detrimental to the filtering cycle
when model uncertainty is low and observation precision is high. In other
configurations, such as those with high model uncertainty, the differences between
@@ -50,7 +50,7 @@
respect to convergence in the strong sense for generating the observation sequence.
The model simulation step sizes are varied in the settings below to demonstrate the
differences between the commonly uses Euler-Maruyama and the more statistically
-robust Runge-Kutta method for SDE integration. See README in `dapper.mods.Lorenz96s`.
+robust Runge-Kutta method for SDE integration. See README in [`mods.Lorenz96s`][].
"""
import numpy as np
@@ -93,7 +93,7 @@ def l96s_tay2_step(x, t, dt, s):
model twin will be generated by on of the wrappers below. The order 2.0
Taylor-Stratonovich discretization scheme implemented here is the basic formulation
which makes a Fourier truncation at p=1 for the Brownian bridge process. See
- `bib.grudzien2020numerical` for full details of the scheme and other versions."""
+ [grudzien2020numerical][] for full details of the scheme and other versions."""
# Infer system dimension
sys_dim = len(x)
diff --git a/dapper/mods/Lorenz96s/grudzien2020.py b/dapper/mods/Lorenz96s/grudzien2020.py
index 50da5464..d6dcd2bc 100644
--- a/dapper/mods/Lorenz96s/grudzien2020.py
+++ b/dapper/mods/Lorenz96s/grudzien2020.py
@@ -1,4 +1,4 @@
-"""Settings as in `bib.grudzien2020numerical`."""
+"""Settings as in [grudzien2020numerical][]."""
import dapper.mods as modelling
from dapper.mods.Lorenz96 import Tplot, x0
diff --git a/dapper/mods/LorenzUV/__init__.py b/dapper/mods/LorenzUV/__init__.py
index c2aba4f6..30f54245 100644
--- a/dapper/mods/LorenzUV/__init__.py
+++ b/dapper/mods/LorenzUV/__init__.py
@@ -1,6 +1,6 @@
"""The 2-scale/layer/speed coupled version of Lorenz-96.
-See `bib.wilks2005effects`
+See [wilks2005effects][]
- U: large amp, low frequency vars: convective events
- V: small amp, high frequency vars: large-scale synoptic events
@@ -45,7 +45,7 @@ def newfun(x, *args, reverse=False, **kwargs):
class model_instance:
"""Use OOP to facilitate having multiple parameter settings simultaneously.
- Default parameters from `bib.wilks2005effects`.
+ Default parameters from [wilks2005effects][].
"""
def __init__(self, nU=8, J=32, F=20, h=1, b=10, c=10):
diff --git a/dapper/mods/LorenzUV/illust_LorenzUV.py b/dapper/mods/LorenzUV/illust_LorenzUV.py
index 8080ec1f..c859163c 100644
--- a/dapper/mods/LorenzUV/illust_LorenzUV.py
+++ b/dapper/mods/LorenzUV/illust_LorenzUV.py
@@ -140,7 +140,10 @@ def tV(zz):
if False:
fig.savefig(
- "docs/imgs/logo_wtxt.png", bbox_inches="tight", pad_inches=0, dpi=200
+ "docs/images/logos/logo_wtxt.png",
+ bbox_inches="tight",
+ pad_inches=0,
+ dpi=200,
)
plt.show()
diff --git a/dapper/mods/LorenzUV/lorenz96.py b/dapper/mods/LorenzUV/lorenz96.py
index aa119ea2..6b75a315 100644
--- a/dapper/mods/LorenzUV/lorenz96.py
+++ b/dapper/mods/LorenzUV/lorenz96.py
@@ -1,4 +1,4 @@
-"""As in `bib.lorenz1996predictability`."""
+"""As in [lorenz1996predictability][]."""
from pathlib import Path
diff --git a/dapper/mods/LorenzUV/wilks05.py b/dapper/mods/LorenzUV/wilks05.py
index 7ef7478a..43a671c3 100644
--- a/dapper/mods/LorenzUV/wilks05.py
+++ b/dapper/mods/LorenzUV/wilks05.py
@@ -1,4 +1,4 @@
-"""Uses `nU`, `J`, `F` as in `dapper.mods.LorenzUV` ie. from `bib.wilks2005effects`.
+"""Uses `nU`, `J`, `F` as in [`mods.LorenzUV`][] ie. from [wilks2005effects][].
Obs settings taken from different places (=> quasi-linear regime).
"""
diff --git a/dapper/mods/LotkaVolterra/__init__.py b/dapper/mods/LotkaVolterra/__init__.py
index fe83f1d5..42f61a82 100644
--- a/dapper/mods/LotkaVolterra/__init__.py
+++ b/dapper/mods/LotkaVolterra/__init__.py
@@ -2,7 +2,7 @@
Refs:
[Wiki](https://en.wikipedia.org/wiki/Competitive_Lotka-Volterra_equations),
-`bib.vano2006chaos`.
+[vano2006chaos][].
"""
import numpy as np
diff --git a/dapper/mods/QG/__init__.py b/dapper/mods/QG/__init__.py
index ee6dc5e7..50c73c09 100644
--- a/dapper/mods/QG/__init__.py
+++ b/dapper/mods/QG/__init__.py
@@ -1,4 +1,4 @@
-"""Quasi-geostraphic 2D flow. Described in detail by `bib.sakov2008deterministic`.
+"""Quasi-geostraphic 2D flow. Described in detail by [sakov2008deterministic][].
Adapted from Pavel Sakov's enkf-matlab package.
diff --git a/dapper/mods/QG/counillon2009.py b/dapper/mods/QG/counillon2009.py
index 1d6f6436..aac81145 100644
--- a/dapper/mods/QG/counillon2009.py
+++ b/dapper/mods/QG/counillon2009.py
@@ -1,4 +1,4 @@
-"""Reproduce experiments from `bib.counillon2009application`."""
+"""Reproduce experiments from [counillon2009application][]."""
import dapper.mods as modelling
from dapper.mods.QG import model_config
diff --git a/dapper/mods/QG/sakov2008.py b/dapper/mods/QG/sakov2008.py
index 604575f1..fc45f5e0 100644
--- a/dapper/mods/QG/sakov2008.py
+++ b/dapper/mods/QG/sakov2008.py
@@ -1,4 +1,4 @@
-"""Reproduce results from `bib.sakov2008deterministic`."""
+"""Reproduce results from [sakov2008deterministic][]."""
import numpy as np
diff --git a/dapper/mods/README.md b/dapper/mods/README.md
index 63b800ac..18cc4422 100644
--- a/dapper/mods/README.md
+++ b/dapper/mods/README.md
@@ -1,40 +1,37 @@
See the README section on
-[test cases (models)](https://github.com/nansencenter/DAPPER#Test-cases-models)
+[test cases (models)](../../index.md#test-cases-models)
for a table overview of the included models.
## Defining your own model
Below is a sugested structuring followed by most models already within DAPPER.
However, you are free to organize your model as you see fit,
-as long as it culminates in the definition of one or more `dapper.mods.HiddenMarkovModel`.
+as long as it culminates in the definition of one or more [`mods.HiddenMarkovModel`][].
For the sake of modularity,
-try not to import stuff from DAPPER outside of `dapper.mods` and `liveplotting`.
+try not to import stuff from DAPPER outside of [`mods`](.) and [`tools.liveplotting`][].
- Make a directory: `my_model`. It does not have to reside within the `dapper/mods` folder,
but make sure to look into some of the other dirs thereunder as examples,
for example `dapper/mods/DoublePendulum`.
-
- Make a file: `my_model/__init__.py` to hold the core workings of the model.
Further details are given [below](#details-on-my_model__init__py), but the
main work lies in defining a `step(x, t, dt)` function
(you can name it however you like, but `step` is the convention),
to implement the dynamical model/system mapping the state `x`
from one time `t` to another `t + dt`.
-
- Make a file: `my_model/demo.py` to run `step` and visually showcase
a simulation of the model without any DA, and verify it's working.
-
- Make a file: `my_model/my_settings_1.py` that defines
- (or "configures", since there is usually little programming logic and flow taking place)
- a complete `dapper.mods.HiddenMarkovModel` ready for a synthetic experiment
- (also called "twin experiment" or OSSE).
-- Once you've made some experiments you believe are noteworthy you should add a
- "suggested settings/tunings" section in comments at the bottom of
- `my_model/my_settings_1.py`, listing some of the relevant DA method
- configurations that you tested, along with the RMSE (or other stats) that
- you obtained for those methods. You will find plenty of examples already in
- DAPPER, used for cross-referenced with literature to verify the workings of DAPPER
- (and the reproducibility of publications).
+ (or "configures", since there is usually little programming logic and flow taking place)
+ a complete [`mods.HiddenMarkovModel`][] ready for a synthetic experiment
+ (also called "twin experiment" or OSSE).
+- Once you've made some experiments you believe are noteworthy you should add a
+ "suggested settings/tunings" section in comments at the bottom of
+ `my_model/my_settings_1.py`, listing some of the relevant DA method
+ configurations that you tested, along with the RMSE (or other stats) that
+ you obtained for those methods. You will find plenty of examples already in
+ DAPPER, used for cross-referenced with literature to verify the workings of DAPPER
+ (and the reproducibility of publications).
### Details on `my_model/__init__.py`
@@ -44,19 +41,19 @@ try not to import stuff from DAPPER outside of `dapper.mods` and `liveplotting`.
number of dimensions (as the input).
See
- - `dapper.mods.Lorenz63`: use of `ens_compatible`.
- - `dapper.mods.Lorenz96`: use of relatively clever slice notation.
- - `dapper.mods.LorenzUV`: use of cleverer slice notation: `...` (ellipsis).
+ - [`mods.Lorenz63`][]: use of `ens_compatible`.
+ - [`mods.Lorenz96`][]: use of relatively clever slice notation.
+ - [`mods.LorenzUV`][]: use of cleverer slice notation: `...` (ellipsis).
Consider pre-defining the slices like so:
+ ```python
+ iiX = (..., slice(None, Nx))
+ iiP = (..., slice(Nx, None))
+ ```
+ to abbreviate the indexing elsewhere.
- iiX = (..., slice(None, Nx))
- iiP = (..., slice(Nx, None))
-
- to abbreviate the indexing elsewhere.
-
- - `dapper.mods.QG`: use of parallelized for loop (map).
+ - [`mods.QG`][]: use of parallelized for loop (map).
- .. note::
+ !!! note
To begin with, test whether the model works on 1 realization,
before running it with several (simultaneously).
Also, start with a small integration time step,
@@ -64,16 +61,16 @@ try not to import stuff from DAPPER outside of `dapper.mods` and `liveplotting`.
Note that the time step might need to be shorter in assimilation,
because it may cause instabilities.
- .. note::
+ !!! note
Most models are defined using simple procedural style.
- However, `dapper.mods.LorenzUV` and `dapper.mods.QG` use OOP,
+ However, [`mods.LorenzUV`][] and [`mods.QG`][] use OOP,
which is perhaps more robust when different
control-variable settings are to be investigated.
The choice is yours.
In parameter estimation problems, the parameters are treated as input
variables to the "forward model". This does not *necessarily* require OOP.
- See `examples/param_estim.py`.
+ See `docs/examples/param_estim.py`.
- Optional: define a suggested/example initial state, `x0`.
This facilitates the specification of initial conditions for different synthetic
diff --git a/dapper/mods/VL20/__init__.py b/dapper/mods/VL20/__init__.py
index 138093ab..cdbe2544 100644
--- a/dapper/mods/VL20/__init__.py
+++ b/dapper/mods/VL20/__init__.py
@@ -1,7 +1,7 @@
"""
Single-scale Lorenz-96 with an added thermodynamic component.
-Refs: `bib.vissio2020mechanics`
+Refs: [vissio2020mechanics][]
"""
import numpy as np
diff --git a/dapper/mods/VL20/demo.py b/dapper/mods/VL20/demo.py
index 9885d30c..55ec3d28 100644
--- a/dapper/mods/VL20/demo.py
+++ b/dapper/mods/VL20/demo.py
@@ -1,6 +1,6 @@
"""Demonstrate the Vissio-Lucarini-20 model.
-Reproduce Hovmoller diagram Fig 4. in `bib.vissio2020mechanics`.
+Reproduce Hovmoller diagram Fig 4. in [vissio2020mechanics][].
"""
import numpy as np
diff --git a/dapper/mods/__init__.py b/dapper/mods/__init__.py
index 723f7a5d..de651d86 100644
--- a/dapper/mods/__init__.py
+++ b/dapper/mods/__init__.py
@@ -1,6 +1,6 @@
"""Contains models included with DAPPER.
-.. include:: ./README.md
+--8<-- "dapper/mods/README.md"
"""
import copy as cp
@@ -30,55 +30,66 @@ class HiddenMarkovModel(struct_tools.NicePrint):
Should contain the details necessary to run synthetic DA experiments,
also known as "twin experiment", or OSSE (observing system simulation experiment).
The synthetic truth and observations may then be obtained by running
- `HiddenMarkovModel.simulate`.
-
- .. note::
- Each model included with DAPPER comes with several examples
- of model settings from the literature.
- See, for example, `dapper.mods.Lorenz63.sakov2012`.
-
- .. warning::
- These example configurations do not necessarily hold a high programming standard,
- as they may have been whipped up at short notice to replicate some experiments,
- and are not intended for re-use.
- Nevertheless, sometimes they are re-used by another configuration script,
- leading to a major gotcha/pitfall: changes made to the imported `HMM` (or
- the model's module itself) also impact the original object (since they
- are mutable and thereby referenced). This *usually* isn't an issue, since
- one rarely imports two/more separate configurations. However, the test suite
- imports all configurations, which might then unintentionally interact.
- To avoid this, you should use the `copy` method of the `HMM`
- before making any changes to it.
-
-
- Parameters
- ----------
- Dyn: `Operator` or dict
- Operator for the dynamics.
- Obs: `Operator` or dict
- Operator for the observations
- Can also be time-dependent, ref `TimeDependentOperator`.
- tseq: `dapper.tools.chronos.Chronology`
- Time sequence of the HMM process.
- X0: `dapper.tools.randvars.RV`
- Random distribution of initial condition
- liveplotters: `list`, optional
- A list of tuples. See example use in function `LPs` of `dapper.mods.Lorenz63`.
- - The first element of the tuple determines if the liveplotter
- is shown by default. If `False`, the liveplotter is only shown when
- included among the `liveplots` argument of `assimilate`
- - The second element in the tuple gives the corresponding liveplotter
- function/class.
- sectors: `dict`, optional
- Labelled indices referring to parts of the state vector.
- When defined, field-mean statistics are computed for each sector.
- Example use can be found in `examples/param_estim.py`
- and `dapper/mods/Lorenz96/miyoshi2011.py`
- name: str, optional
- Label for the `HMM`.
+ [`.simulate`][mods.HiddenMarkovModel.simulate].
+
+ !!! note
+ Each model included with DAPPER comes with several examples
+ of model settings from the literature.
+ See, for example, [`mods.Lorenz63.sakov2012`][].
+
+ !!! warning
+ These example configs do not necessarily hold a high programming standard,
+ as they may have been whipped up at short notice to replicate some experiments,
+ and are not intended for re-use.
+ Nevertheless, sometimes they are re-used by another configuration script,
+ leading to a major gotcha/pitfall: changes made to the imported `HMM` (or
+ the model's module itself) also impact the original object (since they
+ are mutable and thereby referenced). This *usually* isn't an issue, since
+ one rarely imports two/more separate configurations. However, the test suite
+ imports all configurations, which might then unintentionally interact.
+ To avoid this, you should use the `copy` method of the `HMM`
+ before making any changes to it.
"""
- def __init__(self, *args, **kwargs):
+ def __init__(
+ self,
+ Dyn,
+ Obs,
+ tseq,
+ X0,
+ liveplotters=None,
+ sectors=None,
+ name=None,
+ **kwargs,
+ ):
+ """Initialize.
+
+ Parameters
+ ----------
+ Dyn : Operator or dict
+ Operator for the dynamics.
+ Obs : Operator or TimeDependentOperator or dict
+ Operator for the observations
+ Can also be time-dependent, ref `TimeDependentOperator`.
+ tseq : tools.chronos.Chronology
+ Time sequence of the HMM process.
+ X0 : tools.randvars.RV
+ Random distribution of initial condition
+ liveplotters : list, optional
+ A list of tuples. See example use in function `LPs` of [`mods.Lorenz63`][].
+ - The first element of the tuple determines if the liveplotter
+ is shown by default. If `False`, the liveplotter is only shown when
+ included among the `liveplots` argument of `assimilate`
+ - The second element in the tuple gives the corresponding liveplotter
+ function/class.
+ sectors : dict, optional
+ Labelled indices referring to parts of the state vector.
+ When defined, field-mean statistics are computed for each sector.
+ Example use can be found in `docs/examples/param_estim.py`
+ and `dapper/mods/Lorenz96/miyoshi2011.py`
+ name : str, optional
+ Label for the `HMM`.
+ """
# Expected args/kwargs, along with type and default.
attrs = dict(
Dyn=(Operator, None),
@@ -90,11 +101,6 @@ def __init__(self, *args, **kwargs):
name=(str, self._default_name()),
)
- # Transfer args to kwargs
- for arg, kw in zip(args, attrs):
- assert kw not in kwargs, "Could not sort out arguments."
- kwargs[kw] = arg
-
# Un-abbreviate
abbrevs = {"LP": "liveplotters", "loc": "localizer"}
for key in list(kwargs):
@@ -106,9 +112,10 @@ def __init__(self, *args, **kwargs):
assert full not in kwargs, "Could not sort out arguments."
kwargs[full] = kwargs.pop(key)
- # Convert dict to object
+ # Collect args, kwargs.
for key, (type_, default) in attrs.items():
- val = kwargs.get(key, default)
+ val = locals()[key] or kwargs.get(key, default)
+ # Convert dict to object
if not isinstance(val, type_) and val is not None:
val = type_(**val)
kwargs[key] = val
@@ -177,7 +184,8 @@ class TimeDependentOperator:
The time instance should be specified by `ko`,
i.e. the index of an observation time.
- Examples: `examples/time-dep-obs-operator.py` and `dapper/mods/QG/sakov2008.py`.
+ Examples: `docs/examples/time-dep-obs-operator.py`
+ and `dapper/mods/QG/sakov2008.py`.
"""
def __init__(self, **kwargs):
@@ -216,14 +224,16 @@ class Operator(struct_tools.NicePrint):
Parameters
----------
- M: int
+ M : int
Length of output vectors.
- model: function
+ model : function
The actual operator.
- noise: RV, optional
+ noise : RV, optional
The associated additive noise. The noise can also be a scalar or an
array, producing `GaussRV(C=noise)`.
+ Note
+ ----
Any remaining keyword arguments are written to the object as attributes.
"""
diff --git a/dapper/mods/integration.py b/dapper/mods/integration.py
index 46d4da87..f4843834 100644
--- a/dapper/mods/integration.py
+++ b/dapper/mods/integration.py
@@ -19,7 +19,7 @@ def rk4(f, x, t, dt, stages=4, s=0):
For SDEs with additive noise (`s>0`), the order of convergence
(both weak and strong) is 1 for `stages` equal to 1 or 4.
These correspond to the classic Euler-Maruyama scheme and the Runge-Kutta
- scheme for S-ODEs respectively, see `bib.grudzien2020numerical`
+ scheme for S-ODEs respectively, see [grudzien2020numerical][]
for a DA-specific discussion on integration schemes and their discretization errors.
Parameters
@@ -98,7 +98,7 @@ def with_recursion(func, prog=False):
Return a version of `func` whose 2nd argument (`k`)
specifies the number of times to times apply func on its output.
- .. warning:: Only the first argument to `func` will change,
+ !!! warning Only the first argument to `func` will change,
so, for example, if `func` is `step(x, t, dt)`,
it will get fed the same `t` and `dt` at each iteration.
@@ -115,6 +115,7 @@ def with_recursion(func, prog=False):
fun_k : function
A function that returns the sequence generated by recursively
running `func`, i.e. the trajectory of system's evolution.
+
Examples
--------
>>> def dxdt(x):
@@ -154,10 +155,13 @@ def integrate_TLM(TLM, dt, method='approx'):
- the Jacobian of the step func.
- the integral of (with *M* as the TLM):
- $$ \frac{d U}{d t} = M U, \quad U_0 = I .$$
- .. note:: the tangent linear model (TLM)
- is assumed constant (for each `method` below).
+ $$
+ \frac{d U}{d t} = M U, \quad U_0 = I .
+ $$
+
+ !!! tip "The tangent linear model (TLM)"
+ is assumed constant (for each `method` below).
Parameters
----------
@@ -166,7 +170,9 @@ def integrate_TLM(TLM, dt, method='approx'):
- `'approx'` : derived from the forward-euler scheme.
- `'rk4'` : higher-precision approx.
- `'analytic'`: exact.
- .. warning:: 'analytic' typically requries higher inflation in the ExtKF.
+
+ !!! warning
+ "'analytic' typically requries higher inflation in the ExtKF."
See Also
--------
diff --git a/dapper/mods/utils.py b/dapper/mods/utils.py
index f5beb1d5..1aeb223c 100644
--- a/dapper/mods/utils.py
+++ b/dapper/mods/utils.py
@@ -42,12 +42,12 @@ def ens_compatible(func):
This is helpful to make functions compatible with both 1d and 2d ndarrays.
- .. note:: This is not `the_way™` -- other tricks (ref `dapper.mods`)
- are sometimes more practical.
+ !!! tip "This is not `the_way™`"
+ Other tricks (ref [`mods`][]) are sometimes more practical.
Examples
--------
- `dapper.mods.Lorenz63.dxdt`, `dapper.mods.DoublePendulum.dxdt`
+ [`mods.Lorenz63.dxdt`][], [`mods.DoublePendulum.dxdt`][]
See Also
--------
@@ -70,15 +70,16 @@ def linear_model_setup(ModelMatrix, dt0):
r"""Make the Dyn/Obs field of a HMM representing a linear model.
Let *M* be the model matrix. Then
- .. math::
- x(t+dt) = M^{dt/dt0} x(t),
+ $$
+ x(t+dt) = M^{dt/dt0} x(t),
+ $$
i.e.
- .. math::
-
- \frac{dx}{dt} = \frac{\log(M)}{dt0} x(t).
+ $$
+ \frac{dx}{dt} = \frac{\log(M)}{dt0} x(t).
+ $$
In typical use, `dt0==dt` (where `dt` is defined by the chronology).
Anyways, `dt` must be an integer multiple of `dt0`.
@@ -115,14 +116,14 @@ def direct_obs_matrix(Nx, obs_inds):
Parameters
----------
- Nx: int
+ Nx : int
Length of state vector
- obs_inds: ndarray
+ obs_inds : ndarray
Indices of elements of the state vector that are (directly) observed.
Returns
-------
- H: ndarray
+ H : ndarray
The observation matrix for direct partial observations.
"""
Ny = len(obs_inds)
@@ -142,14 +143,14 @@ def partial_Id_Obs(Nx, obs_inds):
Parameters
----------
- Nx: int
+ Nx : int
Length of state vector
- obs_inds: ndarray
+ obs_inds : ndarray
The observed indices.
Returns
-------
- Obs: dict
+ Obs : dict
Observation operator including size of the observation space,
observation operator/model and tangent linear observation operator
"""
@@ -180,12 +181,12 @@ def Id_Obs(Nx):
Parameters
----------
- Nx: int
+ Nx : int
Length of state vector
Returns
-------
- Obs: dict
+ Obs : dict
Observation operator including size of the observation space,
observation operator/model and tangent linear observation operator
"""
@@ -197,18 +198,18 @@ def linspace_int(Nx, Ny, periodic=True):
Parameters
----------
- Nx: int
+ Nx : int
Range of integers
- Ny: int
+ Ny : int
Number of integers
- periodic: bool, optional
+ periodic : bool, optional
Whether the vector is periodic.
Determines if `Nx == 0`.
Default: True
Returns
-------
- integers: ndarray
+ integers : ndarray
The list of integers.
Examples
diff --git a/dapper/stats.py b/dapper/stats.py
index 2e81e623..cf9cdebc 100644
--- a/dapper/stats.py
+++ b/dapper/stats.py
@@ -1,15 +1,15 @@
"""Statistics for the assessment of DA methods.
-`Stats` is a data container for ([mostly] time series of) statistics.
+[`Stats`][stats.Stats] is a data container for ([mostly] time series of) statistics.
It comes with a battery of methods to compute the default statistics.
-`Avrgs` is a data container *for the same statistics*,
+[`Avrgs`][stats.Avrgs] is a data container *for the same statistics*,
but after they have been averaged in time (after the assimilation has finished).
-Instances of these objects are created by `dapper.da_methods.da_method`
+Instances of these objects are created by [`da_methods.da_method`][]
(i.e. "`xp`") objects and written to their `.stats` and `.avrgs` attributes.
-.. include:: ../docs/stats_etc.md
+--8<-- "dapper/stats_etc.md"
"""
import warnings
@@ -127,7 +127,8 @@ def new_series(self, name, shape, length="FAUSt", field_mean=False, **kws):
"""Create (and register) a statistics time series, initialized with `nan`s.
If `length` is an integer, a `DataSeries` (a trivial subclass of
- `numpy.ndarray`) is made. By default, though, a `series.FAUSt` is created.
+ `numpy.ndarray`) is made. By default, though, a [tools.series.FAUSt][]
+ is created.
NB: The `sliding_diagnostics` liveplotting relies on detecting `nan`'s
to avoid plotting stats that are not being used.
@@ -173,7 +174,8 @@ def data_series(self):
]
def assess(self, k, ko=None, faus=None, E=None, w=None, mu=None, Cov=None):
- """Common interface for both `Stats.assess_ens` and `Stats.assess_ext`.
+ """Common interface for both [`Stats`.assess_ens][stats.Stats.assess_ens]
+ and [`Stats`.assess_ext][stats.Stats.assess_ext].
The `_ens` assessment function gets called if `E is not None`,
and `_ext` if `mu is not None`.
@@ -438,11 +440,12 @@ def replay(self, figlist="default", speed=np.inf, t1=0, t2=None, **kwargs):
- t1, t2: time window to plot.
- 'figlist' and 'speed': See LivePlot's doc.
- .. note:: `store_u` (whether to store non-obs-time stats) must
- have been `True` to have smooth graphs as in the actual LivePlot.
+ !!! note
+ `store_u` (whether to store non-obs-time stats) must
+ have been `True` to have smooth graphs as in the actual LivePlot.
- .. note:: Ensembles are generally not stored in the stats
- and so cannot be replayed.
+ !!! note
+ Ensembles are generally not stored in the stats and so cannot be replayed.
"""
# Time settings
tseq = self.HMM.tseq
@@ -499,12 +502,13 @@ class Avrgs(series.StatPrint, struct_tools.DotDict):
Embellishments:
- - `dapper.tools.StatPrint`
- - `Avrgs.tabulate`
+ - [`tools.series.StatPrint`][]
+ - [`Avrgs.tabulate`][stats.Avrgs.tabulate]
- `getattr` that supports abbreviations.
"""
def tabulate(self, statkeys=(), decimals=None):
+ """Tabulate using [`tabulate_avrgs`][stats.tabulate_avrgs]"""
columns = tabulate_avrgs([self], statkeys, decimals=decimals)
return tabulate(columns, headers="keys").replace("␣", " ")
@@ -551,7 +555,8 @@ def align_col(col, pad="␣", missingval="", just=">"):
Treats `int`s and fixed-point `float`/`str` especially, aligning on the point.
- Example:
+ Examples
+ --------
>>> xx = [1, 1., 1.234, 12.34, 123.4, "1.2e-3", None, np.nan, "inf", (1, 2)]
>>> print(*align_col(xx), sep="\n")
␣␣1␣␣␣␣
@@ -623,14 +628,14 @@ def unpack_uqs(uq_list, decimals=None):
and may get formatted somehow (e.g. cast to strings) in the output.
If `uq` is `None`, then `None` is inserted in each list.
- Else, `uq` must be an instance of `dapper.tools.rounding.UncertainQtty`.
+ Else, `uq` must be an instance of [`tools.rounding.UncertainQtty`][].
Parameters
----------
- uq_list: list
+ uq_list : list
List of `uq`s.
- decimals: int
+ decimals : int
Desired number of decimals.
Used for (only) the columns "val" and "prec".
Default: `None`. In this case, the formatting is left to the `uq`s.
@@ -703,23 +708,23 @@ def center(E, axis=0, rescale=False):
Parameters
----------
- E: ndarray
+ E : ndarray
Ensemble which going to be inflated
- axis: int, optional
+ axis : int, optional
The axis to be centered. Default: 0
- rescale: bool, optional
+ rescale : bool, optional
If True, inflate to compensate for reduction in the expected variance.
The inflation factor is \(\sqrt{\frac{N}{N - 1}}\)
where N is the ensemble size. Default: False
Returns
-------
- X: ndarray
+ X : ndarray
Ensemble anomaly
- x: ndarray
+ x : ndarray
Mean of the ensemble
"""
x = np.mean(E, axis=axis, keepdims=True)
@@ -735,7 +740,7 @@ def center(E, axis=0, rescale=False):
def mean0(E, axis=0, rescale=True):
- """Like `center`, but only return the anomalies (not the mean).
+ """Like [`center`][stats.center], but only return the anomalies (not the mean).
Uses `rescale=True` by default, which is beneficial
when used to center observation perturbations.
@@ -751,7 +756,7 @@ def inflate_ens(E, factor):
E : ndarray
Ensemble which going to be inflated
- factor: `float`
+ factor : `float`
Inflation factor
Returns
@@ -773,10 +778,10 @@ def weight_degeneracy(w, prec=1e-10):
Parameters
----------
- w: ndarray
+ w : ndarray
Importance weights. Must sum to 1.
- prec: float, optional
+ prec : float, optional
Tolerance of the distance between w and one. Default:1e-10
Returns
@@ -792,21 +797,21 @@ def unbias_var(w=None, N_eff=None, avoid_pathological=False):
Parameters
----------
- w: ndarray, optional
+ w : ndarray, optional
Importance weights. Must sum to 1.
Only one of `w` and `N_eff` can be `None`. Default: `None`
- N_eff: float, optional
+ N_eff : float, optional
The "effective" size of the weighted ensemble.
If not provided, it is computed from the weights.
The unbiasing factor is $$ N_{eff} / (N_{eff} - 1) $$.
- avoid_pathological: bool, optional
+ avoid_pathological : bool, optional
Avoid weight collapse. Default: `False`
Returns
-------
- ub: float
+ ub : float
factor used to unbiasing variance
Reference
diff --git a/docs/stats_etc.md b/dapper/stats_etc.md
similarity index 58%
rename from docs/stats_etc.md
rename to dapper/stats_etc.md
index fd870ca9..8c22a5dd 100644
--- a/docs/stats_etc.md
+++ b/dapper/stats_etc.md
@@ -2,15 +2,17 @@
List them using
->>> list(vars(xp.stats))
-... list(vars(xp.avrgs))
+```python
+list(vars(xp.stats))
+list(vars(xp.avrgs))
+```
### The `FAUSt` key/attribute
The time series of statistics (the attributes of `.stats`) may have attributes
`.f`, `.a`, `.s`, `.u`, referring to whether the statistic is for a "forecast",
"analysis", or "smoothing" estimate (as is decided when the calls to
-`Stats.assess` is made), or a "universal" (forecast, but at intermediate
+[Stats.assess][stats.Stats.assess] is made), or a "universal" (forecast, but at intermediate
[non-obs.-time]) estimate.
The same applies for the time-averages of `.avrgs`.
@@ -20,7 +22,14 @@ The same applies for the time-averages of `.avrgs`.
The statistics are also averaged in space.
This is done according to the methods listed in `dpr.rc.field_summaries`.
-.. note::
+!!! note
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod
+ nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor
+ massa, nec semper lorem quam in massa.
+
+!!! note
+
Although sometimes pretty close, `rmv` (a.k.a. `spread.rms`) is not (supposed
to be) an un-biased estimator of `rmse` (a.k.a. `err.rms`). This is because
of the square roots involved in the field summary. Instead, `spread.ms` (i.e.
@@ -30,10 +39,12 @@ This is done according to the methods listed in `dpr.rc.field_summaries`.
If the `HiddenMarkovModel` has the attribute `.sectors` with value, e.g.,
->>> HMM.sectors = {
-... "ocean": inds_of_state_of_the_ocean,
-... "atmos": inds_of_state_of_the_atmosphere,
-... }
+```python
+HMM.sectors = {
+ "ocean": inds_of_state_of_the_ocean,
+ "atmos": inds_of_state_of_the_atmosphere,
+}
+```
then `.stats.rms` and `.avrgs.rms` will also have attributes
named after the keys of `HMM.sectors`, e.g. `stats.err.rms.ocean`.
@@ -41,28 +52,38 @@ This also goes for any other (than `rms`) type of field summary method.
## Declaring new, custom statistics
-Only the time series created with `Stats.new_series` will be in the format
-operated on by `Stats.average_in_time`. For example, create `ndarray` of
+Only the time series created with [Stats.new_series][stats.Stats.new_series] will be in the format
+operated on by [Stats.average_in_time][stats.Stats.average_in_time]. For example, create `ndarray` of
length `Ko+1` to hold the time series of estimated inflation values:
->>> self.stats.new_series('infl', 1, Ko+1)
+```python
+self.stats.new_series('infl', 1, Ko+1)
+```
Alternatively you can overwrite a default statistic; for example:
->>> error_time_series_a = xx - ensemble_time_series_a.mean(axis=1)
-... self.stats.err.rms.a = np.sqrt(np.mean(error_time_series_a**2, axis=-1))
+```python
+error_time_series_a = xx - ensemble_time_series_a.mean(axis=1)
+self.stats.err.rms.a = np.sqrt(np.mean(error_time_series_a**2, axis=-1))
+```
Of course, you could just do this
->>> self.stats.my_custom_stat = value
+```python
+self.stats.my_custom_stat = value
+```
-However, `dapper.xp_launch.run_experiment` (without `free=False`) will delete
+However, [`xp_launch.run_experiment`][] (without `free=False`) will delete
the `Stats` object from `xp` after the assimilation, in order to save memory.
Therefore, in order to have `my_custom_stat` be available among `xp.avrgs`, it
must be "registered":
->>> self.stats.stat_register.append("my_custom_stat")
+```python
+self.stats.stat_register.append("my_custom_stat")
+```
Alternatively, you can do both at once
->>> self.stat("my_custom_stat", value)
+```python
+self.stat("my_custom_stat", value)
+```
diff --git a/dapper/tools/chronos.py b/dapper/tools/chronos.py
index fd169805..e61f3e28 100644
--- a/dapper/tools/chronos.py
+++ b/dapper/tools/chronos.py
@@ -24,9 +24,9 @@ class Chronology:
kko: 2 4 6
[----dko------]
- .. warning:: By convention, there is no obs at 0.
- This is hardcorded in DAPPER,
- whose cycling starts by the forecast.
+ !!! warning
+ By convention, there is no obs at 0.
+ This is hardcorded in DAPPER, whose cycling starts by the forecast.
Identities (subject to precision):
diff --git a/dapper/tools/colors.py b/dapper/tools/colors.py
index b55c1be2..939f704a 100644
--- a/dapper/tools/colors.py
+++ b/dapper/tools/colors.py
@@ -39,7 +39,8 @@ def color_text(text, *color_codes):
def coloring(*color_codes):
"""Color printing using 'with'.
- Example:
+ Examples
+ --------
>>> with coloring(colorama.Fore.GREEN):
... print("--- This is in color ---") # doctest: +SKIP
"""
diff --git a/dapper/tools/linalg.py b/dapper/tools/linalg.py
index c307847c..612951dc 100644
--- a/dapper/tools/linalg.py
+++ b/dapper/tools/linalg.py
@@ -39,11 +39,11 @@ def tsvd(A, threshold=0.99999, avoid_pathological=True):
Parameters
----------
- avoid_pathological: bool
+ avoid_pathological : bool
Avoid truncating (e.g.) the identity matrix.
NB: only applies for float threshold.
- threshold: float or int
+ threshold : float or int
- if `float`, `< 1.0` then "rank" = lowest number
such that the "energy" retained >= threshold
diff --git a/dapper/tools/liveplotting.py b/dapper/tools/liveplotting.py
index 55dc8456..c147c20b 100644
--- a/dapper/tools/liveplotting.py
+++ b/dapper/tools/liveplotting.py
@@ -1,14 +1,14 @@
"""On-line (live) plots of the DA process for various models and methods.
Liveplotters are given by a list of tuples as property or arguments in
-`dapper.mods.HiddenMarkovModel`.
+[`mods.HiddenMarkovModel`][].
- The first element of the tuple determines whether the liveplotter is shown if
the names of liveplotters are not given by `liveplots` argument in
`assimilate`.
- The second element in the tuple gives the corresponding liveplotter
-function/class. See example of function `LPs` in `dapper.mods.Lorenz63`.
+function/class. See example of function `LPs` in [`mods.Lorenz63`][].
The liveplotters can be fine-tuned by each DA experiments via argument of
`liveplots` when calling `assimilate`.
diff --git a/dapper/tools/localization.py b/dapper/tools/localization.py
index 0cc16c46..f03f0076 100644
--- a/dapper/tools/localization.py
+++ b/dapper/tools/localization.py
@@ -20,13 +20,13 @@ def pairwise_distances(A, B=None, domain=None):
Parameters
----------
- A: array of shape `(nPoints, nDims)`.
+ A : array of shape `(nPoints, nDims)`.
A collection of points.
- B:
+ B :
Same as `A`, but `nPoints` can differ.
- domain: tuple
+ domain : tuple
Assume the domain is a **periodic** hyper-rectangle whose
edges along dimension `i` span from 0 to `domain[i]`.
NB: Behaviour not defined if `any(A.max(0) > domain)`, and likewise for `B`.
@@ -221,8 +221,8 @@ def rectangular_partitioning(shape, steps, do_ind=True):
Parameters
----------
- shape: (len(grid[dim]) for dim in range(ndim))
- steps: (step_len[dim] for dim in range(ndim))
+ shape : (len(grid[dim]) for dim in range(ndim))
+ steps : (step_len[dim] for dim in range(ndim))
Returns
-------
diff --git a/dapper/tools/matrices.py b/dapper/tools/matrices.py
index d2bc6a26..a3627c50 100644
--- a/dapper/tools/matrices.py
+++ b/dapper/tools/matrices.py
@@ -137,8 +137,8 @@ def funm_psd(a, fun, check_finite=False):
Adapted from `sla.funm` doc.
- Example
- -------
+ Examples
+ --------
>>> def sqrtm_psd(A):
... return funm_psd(A, sqrt)
"""
@@ -151,8 +151,8 @@ def funm_psd(a, fun, check_finite=False):
def chol_reduce(Right):
"""Return rnk-by-ndim R such that `R.T@R - R.T@R ≈ 0`.
- Example
- -------
+ Examples
+ --------
>>> from dapper.stats import mean0
>>> X = mean0(np.random.randn(20, 5), axis=1)
>>> C = X.T @ X
diff --git a/dapper/tools/multiproc.py b/dapper/tools/multiproc.py
index 98caaf31..2bf1d9b4 100644
--- a/dapper/tools/multiproc.py
+++ b/dapper/tools/multiproc.py
@@ -48,18 +48,18 @@ def Pool(NPROC=None):
so you likely want to re-use a pool rather than repeatedly creating one.
Consider using `functools.partial` to fix kwargs.
- .. note::
+ !!! note
In contrast to *reading*, in-place writing does not work with multiprocessing.
This changes with "shared" arrays, but that has not been tested here.
By contrast, multi*threading* shares the process memory,
but was significantly slower in the tested (pertinent) cases.
- .. warning::
+ !!! warning
`multiprocessing` does not mix with `matplotlib`, so ensure `func` does not
reference `xp.stats.LP_instance`. In fact, `func` should not reference `xp`
at all, because it takes time to serialize.
- See example use in `dapper.mods.QG` and `dapper.da_methods.ensemble.LETKF`.
+ See example use in [`mods.QG`][] and [`da_methods.ensemble.LETKF`][].
"""
if NPROC == False: # noqa: E712
# Yield plain old map
diff --git a/dapper/tools/randvars.py b/dapper/tools/randvars.py
index f0e5f134..8530f89f 100644
--- a/dapper/tools/randvars.py
+++ b/dapper/tools/randvars.py
@@ -149,8 +149,8 @@ def __init__(self, mu=0, C=0, M=None):
def sample(self, N):
"""Sample N realizations. Returns N-by-M (ndim) sample matrix.
- Example
- -------
+ Examples
+ --------
>>> plt.scatter(*(UniRV(C=randcov(2)).sample(10**4).T)) # doctest: +SKIP
"""
if self.C == 0:
diff --git a/dapper/tools/remote/uplink.py b/dapper/tools/remote/uplink.py
index 93f8ff75..cd5b935b 100644
--- a/dapper/tools/remote/uplink.py
+++ b/dapper/tools/remote/uplink.py
@@ -370,7 +370,8 @@ def get_ip(instance):
def sub_run(*args, check=True, capture_output=True, text=True, **kwargs):
r"""Do `subprocess.run`, with responsive defaults.
- Examples:
+ Examples
+ --------
>>> gitfiles = sub_run(["git", "ls-tree", "-r", "--name-only", "HEAD"]) # or:
>>> # gitfiles = sub_run("git ls-tree -r --name-only HEAD", shell=True)
"""
diff --git a/dapper/tools/rounding.py b/dapper/tools/rounding.py
index f1ae2bf7..ab3558c9 100644
--- a/dapper/tools/rounding.py
+++ b/dapper/tools/rounding.py
@@ -17,7 +17,8 @@ class UncertainQtty:
However, this class in itself does not define the `prec` attribute
by anything else than what it does: impact the rounding & printing of `val`.
- Examples:
+ Examples
+ --------
>>> for c in [.01, .1, .2, .9, 1]:
... print(UncertainQtty(1.2345, c))
1.23 ±0.01
@@ -74,7 +75,9 @@ def round(self=1.0): # noqa
return v, c
def __str__(self):
- """Returns 'val ±prec', using `UncertainQtty.round` and some finesse."""
+ """Returns 'val ±prec', using [tools.rounding.UncertainQtty.round][]
+
+ and some finesse."""
v, c = self.round()
if np.isnan(c):
@@ -122,12 +125,12 @@ def np_vectorize(f):
Parameters
----------
- f: callable
+ f : callable
Your function.
Returns
-------
- vectorized: callable
+ vectorized : callable
Your function, now element-wise applicable to an iterable.
"""
vectorized = np.vectorize(f)
@@ -148,6 +151,8 @@ def _round2prec(num, prec):
This function is left here just for reference. Use `round2` instead.
+ Examples
+ --------
The issue is that:
>>> _round2prec(0.7,.1)
0.7000000000000001
@@ -190,7 +195,7 @@ def round2(x, prec=1.0):
----------
x : array_like
Value to be rounded.
- prec: float
+ prec : float
Precision, before prettify, which is given by
$$ \text{prec} = 10^{\text{floor}(-\log_{10}|\text{prec}|)} $$
@@ -257,12 +262,12 @@ def is_whole(x, **kwargs):
Parameters
----------
- x: float or ndarray
+ x : float or ndarray
Values to be checked
Returns
-------
- l: bool
+ l : bool
True if rounded x is close to x, otherwise False
"""
return np.isclose(x, round(x), **kwargs)
diff --git a/dapper/tools/series.py b/dapper/tools/series.py
index 82a186bb..42085290 100644
--- a/dapper/tools/series.py
+++ b/dapper/tools/series.py
@@ -65,7 +65,7 @@ def mean_ratio(xx):
def estimate_corr_length(xx):
r"""Estimate the correlation length of a time series.
- For explanation, see `dapper.mods.LA.homogeneous_1D_cov`.
+ For explanation, see [`mods.LA.homogeneous_1D_cov`][].
Also note that, for exponential corr function, as assumed here,
$$\text{corr}(L) = \exp(-1) \approx 0.368$$
@@ -210,8 +210,9 @@ class FAUSt(DataSeries, StatPrint):
self[k,'u']
self[k,whatever,'u']
- .. note:: If a data series only pertains to analysis times,
- then you should use a plain np.array instead.
+ !!! note
+ If a data series only pertains to analysis times, then you should use a plain
+ np.array instead.
"""
def __init__(self, K, Ko, item_shape, store_u, store_s, **kwargs):
diff --git a/dapper/tools/viz.py b/dapper/tools/viz.py
index c14918c2..07cc3094 100644
--- a/dapper/tools/viz.py
+++ b/dapper/tools/viz.py
@@ -27,9 +27,9 @@ def setup_wrapping(M, periodicity=None):
Parameters
----------
- M: int
+ M : int
Length of the periodic domain
- periodicity: bool, optional
+ periodicity : bool, optional
The mode of the wrapping.
"+1": the first element is appended after the last.
"+/-05": adding the midpoint of the first and last elements.
@@ -37,9 +37,9 @@ def setup_wrapping(M, periodicity=None):
Returns
-------
- ii: ndarray
+ ii : ndarray
indices of periodic domain
- wrap: func
+ wrap : func
transform non-periodic data into periodic data
"""
if periodicity in (None, True):
@@ -77,24 +77,24 @@ def amplitude_animation(
Parameters
----------
- EE: ndarray
+ EE : ndarray
Ensemble arry of the shape (K, N, Nx).
K is the length of time, N is the ensemble size, and
Nx is the length of state vector.
- dt: float
+ dt : float
Time interval of each frame.
- interval: float, optional
+ interval : float, optional
Delay between frames in milliseconds. Defaults to 200.
- periodicity: bool, optional
+ periodicity : bool, optional
The mode of the wrapping.
"+1": the first element is appended after the last.
"+/-05": adding the midpoint of the first and last elements.
Default: "+1"
- blit: bool, optional
+ blit : bool, optional
Controls whether blitting is used to optimize drawing. Default: True
- fignum: int, optional
+ fignum : int, optional
Figure index. Default: None
- repeat: bool, optional
+ repeat : bool, optional
If True, repeat the animation. Default: False
"""
fig, ax = place.freshfig(fignum or "Amplitude animation")
@@ -133,15 +133,15 @@ def xtrema(xx, axis=None):
Parameters
----------
- xx: ndarray
- axis: int, optional
+ xx : ndarray
+ axis : int, optional
Specific axis for min and max. Defaults: None
Returns
-------
- a: float
+ a : float
min value
- b: float
+ b : float
max value
"""
a = np.nanmin(xx, axis)
@@ -154,21 +154,21 @@ def stretch(a, b, factor=1, int_=False):
Parameters
----------
- a: float
+ a : float
Lower bound of domain.
- b: float
+ b : float
Upper bound of domain.
- factor: float, optional
+ factor : float, optional
Streching factor. Defaults: 1
- int_: bool, optional
+ int_ : bool, optional
If True, the domain bounds are integer.
Defaults: False
Returns
-------
- a: float
+ a : float
Lower bound of domain.
- b: float
+ b : float
Upper bound of domain.
"""
c = (a + b) / 2
@@ -185,12 +185,12 @@ def set_ilim(ax, i, Min=None, Max=None):
Parameters
----------
- ax: matplotlib.axes
- i: int
+ ax : matplotlib.axes
+ i : int
1: x-axis; 2: y-axis; 3: z-axis
- Min: float, optional
+ Min : float, optional
Lower bound limit. Defaults: None
- Max: float, optional
+ Max : float, optional
Upper bound limit. Defaults: None
"""
if i == 0:
@@ -210,20 +210,20 @@ def estimate_good_plot_length(xx, tseq=None, mult=100):
Parameters
----------
- xx: ndarray
+ xx : ndarray
Plotted array
- tseq: `dapper.tools.chronos.Chronology`, optional
+ tseq : [`tools.chronos.Chronology`][], optional
object with property dko. Defaults: None
- mult: int, optional
+ mult : int, optional
Number of waves for plotting. Defaults: 100
Returns
-------
- K: int
+ K : int
length for plotting
- Example
- -------
+ Examples
+ --------
>>> K_lag = estimate_good_plot_length(stats.xx, tseq, mult=80) # doctest: +SKIP
"""
if xx.ndim == 2:
@@ -319,9 +319,9 @@ def plot_hovmoller(xx, tseq=None):
Parameters
----------
- xx: ndarray
+ xx : ndarray
Plotted array
- tseq: `dapper.tools.chronos.Chronology`, optional
+ tseq : [`tools.chronos.Chronology`][], optional
object with property dko. Defaults: None
"""
fig, ax = place.freshfig("Hovmoller", figsize=(4, 3.5))
@@ -352,15 +352,15 @@ def integer_hist(E, N, centrd=False, weights=None, **kwargs):
Parameters
----------
- E: ndarray
+ E : ndarray
Ensemble array.
- N: int
+ N : int
Number of histogram bins.
- centrd: bool, optional
+ centrd : bool, optional
If True, each bin is centered in the midpoint. Default: False
- weights: float, optional
+ weights : float, optional
Weights for histogram. Default: None
- kwargs: dict
+ kwargs : dict
keyword arguments for matplotlib.hist
"""
ax = plt.gca()
@@ -374,10 +374,10 @@ def not_available_text(ax, txt=None, fs=20):
Parameters
----------
- ax: matplotlib.axes
- txt: str, optional
+ ax : matplotlib.axes
+ txt : str, optional
Printed text. Defaults: '[Not available]'
- fs: float, optional
+ fs : float, optional
Font size. Defaults: 20.
"""
if txt is None:
@@ -401,16 +401,17 @@ def plot_err_components(stats):
Parameters
----------
- stats: `dapper.stats.Stats`
-
- .. note::
- it was chosen to `plot(ii, mean_in_time(abs(err_i)))`,
- and thus the corresponding spread measure is MAD.
- If one chose instead: `plot(ii, std_spread_in_time(err_i))`,
- then the corresponding measure of spread would have been `spread`.
- This choice was made in part because (wrt. subplot 2)
- the singular values (`svals`) correspond to rotated MADs,
- and because `rms(umisf)` seems too convoluted for interpretation.
+ stats : stats.Stats
+
+ Note
+ ----
+ It was chosen to `plot(ii, mean_in_time(abs(err_i)))`,
+ and thus the corresponding spread measure is MAD.
+ If one chose instead: `plot(ii, std_spread_in_time(err_i))`,
+ then the corresponding measure of spread would have been `spread`.
+ This choice was made in part because (wrt. subplot 2)
+ the singular values (`svals`) correspond to rotated MADs,
+ and because `rms(umisf)` seems too convoluted for interpretation.
"""
fig, (ax0, ax1, ax2) = place.freshfig("Error components", figsize=(6, 6), nrows=3)
@@ -466,7 +467,7 @@ def plot_rank_histogram(stats):
Parameters
----------
- stats: `dapper.stats.Stats`
+ stats: stats.Stats
"""
tseq = stats.HMM.tseq
@@ -512,12 +513,12 @@ def axis_scale_by_array(ax, arr, axis="y", nbins=3):
Parameters
----------
- ax: matplotlib.axes
- arr: ndarray
+ ax : matplotlib.axes
+ arr : ndarray
Array for plotting
- axis: str, optional
+ axis : str, optional
Scaled axis, which can be 'x', 'y' or 'z'. Defaults: 'y'
- nbins: int, optional
+ nbins : int, optional
Number of major ticks. Defaults: 3
"""
yy = array([y for y in arr if y is not None], dtype=float) # rm None
diff --git a/dapper/xp_launch.py b/dapper/xp_launch.py
index 58cc5701..9bc7c3ea 100644
--- a/dapper/xp_launch.py
+++ b/dapper/xp_launch.py
@@ -1,6 +1,6 @@
"""Tools (notably `xpList`) for setup and running of experiments (known as `xp`s).
-See `dapper.da_methods.da_method` for the strict definition of `xp`s.
+See [`da_methods.da_method`][] for the strict definition of `xp`s.
"""
import copy
@@ -34,16 +34,17 @@
def seed_and_simulate(HMM, xp):
"""Default experiment setup (sets seed and simulates truth and obs).
- Used by `xpList.launch` via `run_experiment`.
+ Used by [xp_launch.xpList.launch][]
+ via [xp_launch.run_experiment][].
Parameters
----------
- HMM: HiddenMarkovModel
+ HMM : HiddenMarkovModel
Container defining the system.
- xp: object
- Type: a `dapper.da_methods.da_method`-decorated class.
+ xp : object
+ Type: a [`da_methods.da_method`][]-decorated class.
- .. warning:: `xp.seed` should be set (and `int`).
+ !!! warning "`xp.seed` should be set (and `int`)."
Without `xp.seed` the seed does not get set,
and different `xp`s will use different seeds
@@ -73,9 +74,9 @@ def run_experiment(
fail_gently=False,
**stat_kwargs,
):
- """Used by `xpList.launch` to run each single (DA) experiment ("xp").
+ """Used by [xp_launch.xpList.launch][] to run each single (DA) experiment ("xp").
- This involves steps similar to `examples/basic_1.py`, i.e.:
+ This involves steps similar to `docs/examples/basic_1.py`, i.e.:
- `setup` : Initialize experiment.
- `xp.assimilate` : run DA, pass on exception if fail_gently
@@ -85,25 +86,25 @@ def run_experiment(
Parameters
----------
- xp: object
- Type: a `dapper.da_methods.da_method`-decorated class.
- label: str
+ xp : object
+ Type: a [`da_methods.da_method`][]-decorated class.
+ label : str
Name attached to progressbar during assimilation.
- savedir: str
+ savedir : str
Path of folder wherein to store the experiment data.
- HMM: HiddenMarkovModel
+ HMM : HiddenMarkovModel
Container defining the system.
- free: bool
+ free : bool
Whether (or not) to `del xp.stats` after the experiment is done,
so as to free up memory and/or not save this data
(just keeping `xp.avrgs`).
- statkeys: list
+ statkeys : list
A list of names (possibly in the form of abbreviations) of the
statistical averages that should be printed immediately afther
this xp.
- fail_gently: bool
+ fail_gently : bool
Whether (or not) to propagate exceptions.
- setup: function
+ setup : function
This function must take two arguments: `HMM` and `xp`, and return the `HMM` to
be used by the DA methods (typically the same as the input `HMM`, but could be
modified), and the (typically synthetic) truth and obs time series.
@@ -111,7 +112,7 @@ def run_experiment(
This gives you the ability to customize almost any aspect of the individual
experiments within a batch launch of experiments (i.e. not just the parameters
of the DA. method). Typically you will grab one or more parameter values stored
- in the `xp` (see `dapper.da_methods.da_method`) and act on them, usually by
+ in the `xp` (see [`da_methods.da_method`][]) and act on them, usually by
assigning them to some object that impacts the experiment. Thus, by generating
a new `xp` for each such parameter value you can investigate the
impact/sensitivity of the results to this parameter. Examples include:
@@ -217,17 +218,17 @@ class xpList(list):
Parameters
----------
- args: entries
+ args : entries
Nothing, or a list of `xp`s.
- unique: bool
+ unique : bool
Duplicates won't get appended. Makes `append` (and `__iadd__`) relatively slow.
Use `extend` or `__add__` or `combinator` to bypass this validation.
Also see
--------
- - Examples: `examples/basic_2`, `examples/basic_3`
- - `dapper.xp_process.xpSpace`, which is used for experient result **presentation**,
+ - Examples: `docs/examples/basic_2`, `docs/examples/basic_3`
+ - [`xp_process.xpSpace`][], which is used for experient result **presentation**,
as opposed to this class (`xpList`), which handles **launching** experiments.
"""
@@ -294,11 +295,11 @@ def prep_table(self, nomerge=()):
since there might exist a subset of attributes that) uniquely identify each `xp`
in the list (the `redundant` and `common` can be "squeezed" out).
Thus, a table of the `xp`s does not need to list all of the attributes.
- This function also does the heavy lifting for `xpSpace.squeeze`.
+ This function also does the heavy lifting for [xp_process.xpSpace.squeeze][].
Parameters
----------
- nomerge: list
+ nomerge : list
Attributes that should always be seen as distinct.
"""
@@ -464,7 +465,7 @@ def launch(self, HMM, save_as="noname", mp=False, fail_gently=None, **kwargs):
"""Essentially: `for xp in self: run_experiment(xp, ..., **kwargs)`.
See `run_experiment` for documentation on the `kwargs` and `fail_gently`.
- See `dapper.tools.datafiles.create_run_dir` for documentation `save_as`.
+ See [`tools.datafiles.create_run_dir`][] for documentation `save_as`.
Depending on `mp`, `run_experiment` is delegated as follows:
@@ -481,7 +482,7 @@ def launch(self, HMM, save_as="noname", mp=False, fail_gently=None, **kwargs):
- If this dict field is empty, then all python files
in `sys.path[0]` are uploaded.
- See `examples/basic_2.py` and `examples/basic_3.py` for example use.
+ See `docs/examples/basic_2.py` and `docs/examples/basic_3.py` for example use.
"""
# Parse mp option
if not mp:
@@ -627,14 +628,14 @@ def combinator(param_dict, **glob_dict):
"""Mass creation of `xp`'s by combining the value lists in the `param_dict`.
Returns a function (`for_params`) that creates all possible combinations
- of parameters (from their value list) for a given `dapper.da_methods.da_method`.
+ of parameters (from their value list) for a given [`da_methods.da_method`][].
This is a good deal more efficient than relying on `xpList`'s `unique`. Parameters
- not found among the args of the given DA method are ignored by `for_params`.
- specified as keywords to the `for_params` fix the value
preventing using the corresponding (if any) value list in the `param_dict`.
- .. warning::
+ !!! warning
Beware! If, eg., `infl` or `rot` are in `param_dict`, aimed at the `EnKF`,
but you forget that they are also attributes some method where you don't
actually want to use them (eg. `SVGDF`),
diff --git a/dapper/xp_process.py b/dapper/xp_process.py
index 32c9f230..f090e10d 100644
--- a/dapper/xp_process.py
+++ b/dapper/xp_process.py
@@ -24,7 +24,10 @@ class SparseSpace(dict):
Like a normal `dict`, it can hold any type of objects.
But, since the keys must conform, they effectively follow a coordinate system,
- so that the `dict` becomes a vector **space**. Example:
+ so that the `dict` becomes a vector **space**.
+
+ Examples
+ --------
>>> dct = xpSpace(["x", "y", "z"])
>>> dct[(1, 2, 3)] = "pointA"
@@ -90,7 +93,7 @@ def __init__(self, dims):
Parameters
----------
- dims: list or tuple
+ dims : list or tuple
The attributes defining the coordinate system.
"""
# Define coordinate system
@@ -109,7 +112,7 @@ def update(self, items):
"""Update dict, using the custom `__setitem__` to ensure key conformity.
NB: the `kwargs` syntax is not supported because it only works for keys that
- consist of (a single) string, which is not very interesting for SparseSpace.
+ consist of (a single) string, which is not very interesting for `SparseSpace`.
"""
# See https://stackoverflow.com/a/2588648
# and https://stackoverflow.com/a/2390997
@@ -151,16 +154,17 @@ def __getitem__(self, key):
return super().__getitem__(key)
def __call__(self, **kwargs):
- """Shortcut (syntactic sugar) for `SparseSpace.subspace`."""
+ """Shortcut (syntactic sugar) for [xp_process.SparseSpace.subspace][]."""
return self.subspace(**kwargs)
def subspace(self, **kwargs):
"""Get an affine subspace.
NB: If you're calling this repeatedly (for all values of the same `kwargs`)
- then you should consider using `SparseSpace.nest` instead.
+ then you should consider using [xp_process.SparseSpace.nest][] instead.
- Example:
+ Examples
+ --------
>>> xp_dict.subspace(da_method="EnKF", infl=1, seed=3) # doctest: +SKIP
"""
# Slow version
@@ -177,11 +181,13 @@ def subspace(self, **kwargs):
def coords_matching(self, **kwargs):
"""Get all `coord`s matching kwargs.
- Used by `SparseSpace.label_xSection` and `SparseSpace.subspace`. Unlike the
- latter, this function returns a *list* of *keys* of the *original subspace*.
+ Used by [xp_process.SparseSpace.label_xSection][] and
+ [xp_process.SparseSpace.subspace][].
+ Unlike the latter, this function returns a *list* of *keys*
+ of the *original subspace*.
- Note that the `missingval` shenanigans of `xpList.inds` are here unnecessary
- since each coordinate is complete.
+ Note that the `missingval` shenanigans of [xp_launch.xpList.inds][]
+ are here unnecessary since each coordinate is complete.
"""
def match(coord):
@@ -479,8 +485,8 @@ def table_tree(self, statkey, dims, *, costfun=None):
by the mean/tune operations. The `dims['outer']` and `dims['inner']
become the keys for the output hierarchy.
- .. note::
- cannot support multiple `statkey`s because it's not (obviously) meaningful
+ !!! note
+ Cannot support multiple `statkey`s because it's not (obviously) meaningful
when optimizing over `dims['optim']`.
"""
@@ -585,10 +591,10 @@ def print(
Parameters
----------
- statkey: str
+ statkey : str
The statistic to extract from the `xp.avrgs` for each `xp`.
Examples: `"rmse.a"` (i.e. `"err.rms.a"`), `"rmse.ocean.a"`, `"duration"`.
- dims: dict
+ dims : dict
Allots (maps) the dims of `xpSpace` to different roles in the tables.
- The "role" `outer` should list the dims/attributes
@@ -608,7 +614,7 @@ def print(
Equivalently, use `mean=("seed",)`.
It is acceptible to leave this empty: `mean=()` or `mean=None`.
- subcols: bool
+ subcols : bool
If `True`, then subcolumns are added to indicate
- `1σ`: the confidence interval. If `mean=None` is used, this simply reports
@@ -619,16 +625,16 @@ def print(
as defined by `costfun`.
- `☠`: the number of failures (non-finite values) at that point.
- `✓`: the number of successes that go into the value
- decimals: int
+ decimals : int
Number of decimals to print.
If `None`, this is determined for each statistic by its uncertainty.
- costfun: str or function
+ costfun : str or function
Use `'increasing'` (default) or `'decreasing'` to indicate that the optimum
is defined as the lowest or highest value of the `statkey` found.
- squeeze_labels: bool
+ squeeze_labels : bool
Don't include redundant attributes in the line labels.
Caution: `get_style` will not be able to access the eliminated attrs.
- colorize: bool
+ colorize : bool
Add color to tables for readability.
"""
# Title
@@ -766,16 +772,16 @@ def plot(
Parameters
----------
- get_style: function
+ get_style : function
A function that takes an object, and returns a dict of line styles,
usually as a function of the object's attributes.
- title1: anything
+ title1 : anything
Figure title (in addition to the the defaults).
- title2: anything
+ title2 : anything
Figure title (in addition to the defaults). Goes on a new line.
- unique_labels: bool
+ unique_labels : bool
Only show a given line label once, even if it appears in several panels.
- squeeze_labels:
+ squeeze_labels :
Don't include redundant attributes in the labels.
"""
diff --git a/docs/bib/bib.py b/docs/bib/bib.py
deleted file mode 100644
index ff9d2df4..00000000
--- a/docs/bib/bib.py
+++ /dev/null
@@ -1,375 +0,0 @@
-"""Bibliography/references."""
-
-anderson2010non = None
-"""
-**Anderson**, **J L** 2010 A non-Gaussian ensemble filter update
-for data assimilation. *Monthly Weather Review*, 138(11):
-4186–4198.
-"""
-
-anderson2009spatially = None
-"""
-**Anderson**, **J L** 2009 Spatially and temporally varying
-adaptive covariance inflation for ensemble filters. *Tellus A*,
-61(1): 72–83.
-"""
-
-bengtsson2003toward = None
-"""
-**Bengtsson**, **T**, **Snyder**, **C**, and **Nychka**, **D**
-2003 Toward a nonlinear ensemble filter for high-dimensional
-systems. *Journal of Geophysical Research: Atmospheres*, 108(D24).
-"""
-
-bocquet2016localization = None
-"""
-**Bocquet**, **M** 2016 Localization and the iterative ensemble
-Kalman smoother. *Quarterly Journal of the Royal Meteorological
-Society*, 142(695): 1075–1089.
-"""
-
-bocquet2011ensemble = None
-"""
-**Bocquet**, **M** 2011 Ensemble Kalman filtering without the
-intrinsic need for inflation. *Nonlinear Processes in Geophysics*,
-18(5): 735–750.
-"""
-
-bocquet2019consistency = None
-"""
-**Bocquet**, **M** and **Farchi**, **A** 2019 On the consistency
-of the local ensemble square root kalman filter perturbation
-update. *Tellus A: Dynamic Meteorology and Oceanography*, 71(1):
-1613142.
-"""
-
-bocquet2010beyond = None
-"""
-**Bocquet**, **M**, **Pires**, **C A**, and **Wu**, **L** 2010
-Beyond Gaussian statistical modeling in geophysical data
-assimilation. *Monthly Weather Review*, 138(8): 2997–3023.
-"""
-
-bocquet2015expanding = None
-"""
-**Bocquet**, **M**, **Raanes**, **P N**, and **Hannart**, **A**
-2015 Expanding the validity of the ensemble Kalman filter without
-the intrinsic need for inflation. *Nonlinear Processes in
-Geophysics*, 22(6): 645–662.
-"""
-
-bocquet2014iterative = None
-"""
-**Bocquet**, **M** and **Sakov**, **P** 2014 An iterative ensemble
-Kalman smoother. *Quarterly Journal of the Royal Meteorological
-Society*, 140(682): 1521–1535.
-"""
-
-bocquet2013joint = None
-"""
-**Bocquet**, **M** and **Sakov**, **P** 2013 Joint state and
-parameter estimation with an iterative ensemble Kalman smoother.
-*Nonlinear Processes in Geophysics*, 20(5): 803–818.
-"""
-
-bocquet2012combining = None
-"""
-**Bocquet**, **M** and **Sakov**, **P** 2012 Combining
-inflation-free and iterative ensemble Kalman filters for strongly
-nonlinear systems. *Nonlinear Processes in Geophysics*, 19(3):
-383–399.
-"""
-
-chen2003bayesian = None
-"""
-**Chen**, **Z** 2003 Bayesian filtering: From Kalman filters to
-particle filters, and beyond. *Statistics*, 182(1): 1–69.
-"""
-
-counillon2009application = None
-"""
-**Counillon**, **F**, **Sakov**, **P**, and **Bertino**, **L**
-2009 Application of a hybrid EnKF-OI to ocean forecasting.
-"""
-
-doucet2001sequential = None
-"""
-**Doucet**, **A**, **De Freitas**, **N**, and **Gordon**, **N**
-2001 *Sequential Monte Carlo methods in practice*. Springer.
-"""
-
-doucet2009tutorial = None
-"""
-**Doucet**, **A** and **Johansen**, **A M** 2009 A tutorial on
-particle filtering and smoothing: Fifteen years later. *Handbook
-of Nonlinear Filtering*, 656–704.
-"""
-
-emerick2012history = None
-"""
-**Emerick**, **A A** and **Reynolds**, **A C** 2012 History
-matching time-lapse seismic data using the ensemble Kalman filter
-with multiple data assimilations. *Computational Geosciences*,
-16(3): 639–659.
-"""
-
-evensen2009ensemble = None
-"""
-**Evensen**, **G** 2009 The ensemble Kalman filter for combined
-state and parameter estimation. *Control Systems, IEEE*, 29(3):
-83–104.
-"""
-
-frei2013mixture = None
-"""
-**Frei**, **M** and **Künsch**, **H R** 2013b Mixture ensemble
-kalman filters. *Computational Statistics & Data Analysis*, 58:
-127–138.
-"""
-
-frei2013bridging = None
-"""
-**Frei**, **M** and **Künsch**, **H R** 2013a Bridging the
-ensemble Kalman and particle filters. *Biometrika*, ast020.
-"""
-
-grudzien2020numerical = None
-"""
-**Grudzien**, **C**, **Bocquet**, **M**, and **Carrassi**, **A**
-2020 On the numerical integration of the lorenz-96 model, with
-scalar additive noise, for benchmark twin experiments.
-*Geoscientific Model Development*, 13(4): 1903–1924.
-"""
-
-harty2021eigenvector = None
-"""
-**Harty**, **T**, **Morzfeld**, **M**, and **Snyder**, **C** 2021
-Eigenvector-spatial localisation. *Tellus A: Dynamic Meteorology
-and Oceanography*, 73(1): 1–18.
-"""
-
-hoteit2015mitigating = None
-"""
-**Hoteit**, **I**, **Pham**, **D-T**, **Gharamti**, **M E**, and
-**Luo**, **X** 2015 Mitigating observation perturbation sampling
-errors in the stochastic EnKF. *Monthly Weather Review*, 143(7):
-2918–2936.
-"""
-
-hunt2007efficient = None
-"""
-**Hunt**, **B R**, **Kostelich**, **E J**, and **Szunyogh**, **I**
-2007 Efficient data assimilation for spatiotemporal chaos: A local
-ensemble transform Kalman filter. *Physica D: Nonlinear
-Phenomena*, 230(1): 112–126.
-"""
-
-karspeck2007experimental = None
-"""
-**Karspeck**, **A R** and **Anderson**, **J L** 2007 Experimental
-implementation of an ensemble adjustment filter for an
-intermediate ENSO model. *Journal of Climate*, 20(18): 4638–4658.
-"""
-
-van2009particle = None
-"""
-**Leeuwen**, **P J** **van** 2009 Particle filtering in
-geophysical systems. *Monthly Weather Review*, 137(12): 4089–4114.
-"""
-
-lei2011moment = None
-"""
-**Lei**, **J** and **Bickel**, **P** 2011 A moment matching
-ensemble filter for nonlinear non-Gaussian data assimilation.
-*Monthly Weather Review*, 139(12): 3964–3973.
-"""
-
-liu2001theoretical = None
-"""
-**Liu**, **J S**, **Chen**, **R**, and **Logvinenko**, **T** 2001
-A theoretical framework for sequential importance sampling with
-resampling. In: *Sequential Monte Carlo Methods in Practice*.
-Springer. pp. 225–246.
-"""
-
-lorenz2005look = None
-"""
-**Lorenz**, **E N** 2005 A look at some details of the growth of
-initial uncertainties. *Tellus A: Dynamic Meteorology and
-Oceanography*, 57(1): 1–11.
-"""
-
-lorenz1996predictability = None
-"""
-**Lorenz**, **E N** 1996 Predictability: A problem partly solved.
-In: *Proc. ECMWF Seminar on Predictability*. Reading, UK. pp.
-1–18.
-"""
-
-lorenz1984irregularity = None
-"""
-**Lorenz**, **E N** 1984 Irregularity: A fundamental property of
-the atmosphere. *Tellus A*, 36(2): 98–110.
-"""
-
-lorenz1998optimal = None
-"""
-**Lorenz**, **E N** and **Emanuel**, **K A** 1998 Optimal sites
-for supplementary weather observations: Simulation with a small
-model. *Journal of the Atmospheric Sciences*, 55(3): 399–414.
-"""
-
-lorenz2005designing = None
-"""
-**Lorenz**, **Edward N** 2005 Designing chaotic models. *Journal
-of the Atmospheric Sciences*, 62(5): 1574–1587.
-"""
-
-mandel2016hybrid = None
-"""
-**Mandel**, **J**, **Bergou**, **E**, **Gürol**, **S**,
-**Gratton**, **S**, and **Kasanický**, **I** 2016 Hybrid
-Levenberg-Marquardt and weak-constraint ensemble Kalman smoother
-method. *Nonlinear Processes in Geophysics*, 23(2): 59–73.
-"""
-
-miyoshi2011gaussian = None
-"""
-**Miyoshi**, **T** 2011 The Gaussian approach to adaptive
-covariance inflation and its implementation with the local
-ensemble transform Kalman filter. *Monthly Weather Review*,
-139(5): 1519–1535.
-"""
-
-pajonk2012deterministic = None
-"""
-**Pajonk**, **O**, **Rosić**, **B V**, **Litvinenko**, **A**, and
-**Matthies**, **H G** 2012 A deterministic filter for non-Gaussian
-Bayesian estimation—applications to dynamical system estimation
-with noisy measurements. *Physica D: Nonlinear Phenomena*, 241(7):
-775–788.
-"""
-
-pinheiro2019efficient = None
-"""
-**Pinheiro**, **F R**, **Leeuwen**, **P J** **van**, and
-**Geppert**, **G** 2019 Efficient nonlinear data assimilation
-using synchronization in a particle filter. *Quarterly Journal of
-the Royal Meteorological Society*, 145(723): 2510–2523.
-"""
-
-raanes2016thesis = None
-"""
-**Raanes**, **Patrick N** 2016 Improvements to ensemble methods
-for data assimilation in the geosciences (PhD thesis). University
-of Oxford.
-"""
-
-raanes2015rts = None
-"""
-**Raanes**, **Patrick Nima** 2016 On the ensemble
-Rauch-Tung-Striebel smoother and its equivalence to the ensemble
-Kalman smoother. *Quarterly Journal of the Royal Meteorological
-Society*, 142(696): 1259–1264.
-"""
-
-raanes2019adaptive = None
-"""
-**Raanes**, **Patrick N**, **Bocquet**, **M**, and **Carrassi**,
-**A** 2019 Adaptive covariance inflation in the ensemble Kalman
-filter by Gaussian scale mixtures. *Quarterly Journal of the Royal
-Meteorological Society*, 145(718): 53–75. DOI:
-https://doi.org/10.1002/qj.3386
-"""
-
-raanes2014ext = None
-"""
-**Raanes**, **P N**, **Carrassi**, **A**, and **Bertino**, **L**
-2015 Extending the square root method to account for model noise
-in the ensemble Kalman filter. *Monthly Weather Review*, 143(10):
-3857–3873.
-"""
-
-raanes2019revising = None
-"""
-**Raanes**, **Patrick Nima**, **Stordal**, **A S**, and
-**Evensen**, **G** 2019 Revising the stochastic iterative ensemble
-smoother. *Nonlinear Processes in Geophysics*, 26(3): 325–338.
-"""
-
-rainwater2013mixed = None
-"""
-**Rainwater**, **S** and **Hunt**, **B** 2013 Mixed-resolution
-ensemble data assimilation. *Monthly weather review*, 141(9):
-3007–3021.
-"""
-
-sakov2008implications = None
-"""
-**Sakov**, **P** and **Oke**, **P R** 2008b Implications of the
-form of the ensemble transformation in the ensemble square root
-filters. *Monthly Weather Review*, 136(3): 1042–1053.
-"""
-
-sakov2008deterministic = None
-"""
-**Sakov**, **P** and **Oke**, **P R** 2008a A deterministic
-formulation of the ensemble Kalman filter: An alternative to
-ensemble square root filters. *Tellus A*, 60(2): 361–371.
-"""
-
-sakov2012iterative = None
-"""
-**Sakov**, **P**, **Oliver**, **D S**, and **Bertino**, **L** 2012
-An iterative EnKF for strongly nonlinear systems. *Monthly Weather
-Review*, 140(6): 1988–2004.
-"""
-
-todter2015second = None
-"""
-**Tödter**, **J** and **Ahrens**, **B** 2015 A second-order exact
-ensemble square root filter for nonlinear data assimilation.
-*Monthly Weather Review*, 143(4): 1347–1367.
-"""
-
-vano2006chaos = None
-"""
-**Vano**, **J A**, **Wildenberg**, **J C**, **Anderson**, **M B**,
-**Noel**, **J K**, and **Sprott**, **J C** 2006 Chaos in
-low-dimensional Lotka–Volterra models of competition.
-*Nonlinearity*, 19(10): 2391.
-"""
-
-vissio2020mechanics = None
-"""
-**Vissio**, **G** and **Lucarini**, **V** 2020 Mechanics and
-thermodynamics of a new minimal model of the atmosphere. *The
-European Physical Journal Plus*, 135(10): 1–21.
-"""
-
-wikle2007bayesian = None
-"""
-**Wikle**, **C K** and **Berliner**, **L M** 2007 A Bayesian
-tutorial for data assimilation. *Physica D: Nonlinear Phenomena*,
-230(1-2): 1–16.
-"""
-
-wiljes2016second = None
-"""
-**Wiljes**, **J** **de**, **Acevedo**, **W**, and **Reich**, **S**
-2016 Second-order accurate ensemble transform particle filters.
-*arXiv preprint arXiv:1608.08179*.
-"""
-
-wilks2005effects = None
-"""
-**Wilks**, **D S** 2005 Effects of stochastic parametrizations in
-the Lorenz’96 system. *Quarterly Journal of the Royal
-Meteorological Society*, 131(606): 389–407.
-"""
-
-zupanski2005maximum = None
-"""
-**Zupanski**, **M** 2005 Maximum likelihood ensemble filter:
-Theoretical aspects. *Monthly Weather Review*, 133(6): 1710–1726.
-"""
diff --git a/docs/bib/bib2md.py b/docs/bib/bib2md.py
new file mode 100644
index 00000000..c6e3534f
--- /dev/null
+++ b/docs/bib/bib2md.py
@@ -0,0 +1,35 @@
+"""Render bibtex file as markdown with keys as headings that can be cross-referenced."""
+
+from pathlib import Path
+
+from pybtex.database import parse_string
+from pybtex.richtext import Tag, Text
+from pybtex.style.formatting.unsrt import Style as UnsrtStyle
+from pybtex.style.formatting.unsrt import field, sentence
+
+HERE = Path(__file__).parent
+
+
+class MyStyle(UnsrtStyle):
+ # default_sorting_style = "author_year_title"
+ def format_title(self, e, which_field, as_sentence=True):
+ formatted_title = field(
+ which_field,
+ apply_func=lambda text: Tag("tt", Text('"', text.capitalize(), '"')),
+ )
+ if as_sentence:
+ return sentence[formatted_title]
+ else:
+ return formatted_title
+
+
+bib = parse_string((HERE / "refs.bib").read_text(), "bibtex")
+
+formatted = MyStyle().format_entries(bib.entries.values())
+
+md = ""
+for entry in formatted:
+ md += f"### `{entry.key}`\n\n"
+ md += entry.text.render_as("markdown") + "\n\n"
+
+(HERE.parent / "references.md").write_text(md)
diff --git a/docs/bib/data-science-journal.csl b/docs/bib/data-science-journal.csl
deleted file mode 100644
index e7f179e9..00000000
--- a/docs/bib/data-science-journal.csl
+++ /dev/null
@@ -1,228 +0,0 @@
-
-
diff --git a/docs/bib/make_bib.py b/docs/bib/make_bib.py
deleted file mode 100755
index 42960c91..00000000
--- a/docs/bib/make_bib.py
+++ /dev/null
@@ -1,93 +0,0 @@
-#!/usr/bin/env python
-
-"""Make bib.py from refs.bib.
-
-Usage:
-
-- Put the references you refer to in dapper's docstrings in refs.bib.
- Consider using `copy_refs.py` for this purpose.
-- Run this to update the bib.py file.
-- Run pdoc.
-
-Note: requires pandoc, and presumably latex too.
-"""
-
-import os
-from pathlib import Path
-from subprocess import run
-from textwrap import dedent
-
-os.chdir(Path(__file__).parent)
-
-
-# Create .md doc with \notice{*}
-open("bib.md", "w").write(r"""
----
-bibliography: refs.bib
-nocite: '@*'
-...
-
-# Bibliography
-""")
-
-
-# Convert to .rst (to process the references)
-# Requires recent version of pandoc
-# NB: Unfortunately, converting directly to .md
-# outputs in a verbose "list-ish" format.
-# Note: for CSL styles see https://editor.citationstyles.org/about/
-run(
- [
- "pandoc",
- "--citeproc",
- "--csl=data-science-journal.csl",
- "-s",
- "bib.md",
- "-o",
- "bib.rst",
- ],
- check=True,
-)
-
-
-# Parse rst
-rst = open("bib.rst").readlines()
-linenumbers = []
-REF_START = " :name: ref-"
-# Get ref names and line numbers
-for lineno, ln in enumerate(rst):
- if REF_START in ln:
- name = ln[len(REF_START) :].strip()
- linenumbers.append((name, lineno))
-# Get ref blocks
-ref_dict = {}
-for i, (name, lineno1) in enumerate(linenumbers):
- try:
- lineno2 = linenumbers[i + 1][1] - 1
- except IndexError:
- lineno2 = len(rst)
- block = rst[lineno1 + 2 : lineno2]
- block = dedent("".join(block)).strip()
- ref_dict[name] = block
-
-
-# Clean up
-Path("bib.rst").unlink()
-Path("bib.md").unlink()
-
-# Sort
-# ref_dict = dict(sorted(ref_dict.items()))
-
-# Write bib.py
-with open("bib.py", "w") as bibfile:
-
- def _print(*a, **b):
- print(*a, **b, file=bibfile)
-
- _print('"""Bibliography/references."""')
- for key, block in ref_dict.items():
- _print("")
- _print(key, "=", "None")
- _print('"""')
- _print(block)
- _print('"""')
diff --git a/docs/bib/refs.bib b/docs/bib/refs.bib
index 3fb8cc2a..856f809a 100644
--- a/docs/bib/refs.bib
+++ b/docs/bib/refs.bib
@@ -412,10 +412,15 @@ @article{rainwater2013mixed
}
@article{counillon2009application,
- title={Application of a hybrid EnKF-OI to ocean forecasting},
- author={Counillon, Fran{\c{c}}ois and Sakov, Pavel and Bertino, Laurent},
- year={2009},
- publisher={Copernicus Publications on behalf of the European Geosciences Union}
+ title={Application of a hybrid `EnKF-OI` to ocean forecasting},
+ author={Counillon, Fran{\c{c}}ois and Sakov, Pavel and Bertino, Laurent},
+ journal={Ocean Science},
+ file={~/P/Refs/articles/counillon2009application.pdf},
+ volume={5},
+ number={4},
+ pages={389--401},
+ year={2009},
+ publisher={Copernicus GmbH}
}
@article{pajonk2012deterministic,
diff --git a/docs/dev_guide.md b/docs/dev_guide.md
index 718d28b2..d2f9f5a1 100644
--- a/docs/dev_guide.md
+++ b/docs/dev_guide.md
@@ -1,3 +1,8 @@
+---
+hide:
+- navigation
+---
+
# Developer guide
## Conventions
@@ -80,29 +85,7 @@ For detailed linting messages, run
ruff check --output-format=concise
```
-.. warning:: Obsolete
- Kept for reference.
-
You may also want to display linting issues in your editor as you code.
-Below is a suggested configuration of VS-Code with the pylance plug-in
-or Vim (with the coc.nvim plug-in with the pyright extension)
-
-```jsonc
-// Put this in settings.json (VS-Code) or ~/.vim/coc-settings.json (For Vim)
-{
- "python.analysis.typeCheckingMode": "off",
- "python.analysis.useLibraryCodeForTypes": true,
- "python.analysis.extraPaths": ["scripts"],
- "python.formatting.provider": "autopep8",
- "python.formatting.autopep8Path": "~/.local/bin/autopep8",
- "python.linting.enabled": true,
- "python.linting.flake8Enabled": true,
- "python.linting.flake8Args": ["lint"],
- "python.linting.flake8Path": "${env:CONDA_PREFIX}/bin/flakeheaven",
- // Without VENV (requires `pip3 install --user flakeheaven flake8-docstrings flake8-bugbear ...`)
- // "python.linting.flake8Path": "[USE PATH PRINTED BY PIP ABOVE]/Python/3.8/bin/flakeheaven",
-}
-```
## Adding to the examples
@@ -111,15 +94,15 @@ as showcasing some feature, new examples should make sure to reproduce some
published literature results. After making the example, consider converting
the script to the Jupyter notebook format (or vice versa) so that the example
can be run on Colab without users needing to install anything (see
-`examples/README.md`). This should be done using the `jupytext` plug-in (with
+`docs/examples/README.md`). This should be done using the `jupytext` plug-in (with
the `lightscript` format), so that the paired files can be kept in synch.
## Documentation
-The documentation is built with `pdoc`, e.g.
+The documentation is built with `mkdocs`, e.g.
```sh
-pdoc -t docs/templates --math --docformat=numpy docs/bib/bib.py docs/dev_guide.py ./dapper
+mkdocs build # use `serve` instead for live preview with hot-reloading
```
##### Hosting
@@ -134,8 +117,9 @@ creates an artefact that is deployed to Github Pages.
In order to add new references,
insert their bibtex into `docs/bib/refs.bib`,
-then run `docs/bib/make_bib.py`,
-which will format and add entries to `docs/bib/bib.py`.
+then run `docs/bib/bib2md.py`
+which will format and add entries to `docs/references.md`
+that can be cited with regular cross-reference syntax, e.g. `[bocquet2010beyond][]`.
## Profiling
diff --git a/docs/dev_guide.py b/docs/dev_guide.py
deleted file mode 100644
index 5b637555..00000000
--- a/docs/dev_guide.py
+++ /dev/null
@@ -1,3 +0,0 @@
-"""
-.. include:: ./dev_guide.md
-"""
diff --git a/examples/README.md b/docs/examples/README.md
similarity index 93%
rename from examples/README.md
rename to docs/examples/README.md
index f4aac7a9..1cdbca2b 100644
--- a/examples/README.md
+++ b/docs/examples/README.md
@@ -3,7 +3,7 @@
Here are some example scripts using DAPPER.
They all consist of one (or more) synthetic experiments.
-Run them using `python examples/the_script.py`,
+Run them using `python docs/examples/the_script.py`,
or with the `%run` command inside `ipython`.
Some of the scripts have also been converted to Jupyter notebooks (`.ipynb`).
@@ -14,7 +14,7 @@ and that it requires a Google login): [![Open In Collab](https://colab.research.
## Description
When adapting the scripts to your needs,
-you should begin with `examples/basic_1.py`
+you should begin with `basic_1.py`
before incorporating the aspects of `basic_2` and `basic_3`.
- `basic_1.py`: A single experiment, with Liveplotting.
diff --git a/docs/examples/basic_1.ipynb b/docs/examples/basic_1.ipynb
new file mode 100644
index 00000000..c635f289
--- /dev/null
+++ b/docs/examples/basic_1.ipynb
@@ -0,0 +1,7446 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "3c5106d3",
+ "metadata": {},
+ "source": [
+ "## A single, interactive, synthetic (\"twin\") experiment\n",
+ "If run as a script in a terminal (i.e. not as a jupyter notebook)\n",
+ "then the liveplotting is *interactive* (can be paused, skipped, etc)."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "fe5f6628",
+ "metadata": {},
+ "source": [
+ "### Imports\n",
+ "NB: On Gooble Colab,\n",
+ "*replace* `%matplotlib notebook` (right below) by\\\n",
+ "`!python -m pip install git+https://github.com/nansencenter/DAPPER.git`"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "89f5e089",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%matplotlib notebook"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "fc681615",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import dapper as dpr\n",
+ "import dapper.da_methods as da"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "7afbdeb6",
+ "metadata": {},
+ "source": [
+ "Generate the same random numbers each time this script is run:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "0e74274d",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dpr.set_seed(3000);"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1957d07f",
+ "metadata": {},
+ "source": [
+ "### Load experiment setup: the hidden Markov model (HMM)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "8b272589",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "HiddenMarkovModel({\n",
+ " 'Dyn': \n",
+ " Operator({\n",
+ " 'M': 3,\n",
+ " 'model':\n",
+ " rk4 integration of \n",
+ " .step at 0x119028720>,\n",
+ " 'noise':\n",
+ " GaussRV({\n",
+ " 'mu': array([0., 0., 0.]),\n",
+ " 'M': 3,\n",
+ " 'C': 0\n",
+ " }),\n",
+ " 'linear': \n",
+ " }),\n",
+ " 'Obs': \n",
+ " CONSTANT operator sepcified by .Op1:\n",
+ " Operator({\n",
+ " 'M': 3,\n",
+ " 'model':\n",
+ " Direct obs. at [0 1 2]\n",
+ " .model at 0x119028c20>,\n",
+ " 'noise':\n",
+ " GaussRV({\n",
+ " 'C': \n",
+ " M: 3\n",
+ " kind: 'diag'\n",
+ " trunc: 1.0\n",
+ " rk: 3\n",
+ " full:\n",
+ " [[2. 0. 0.]\n",
+ " [0. 2. 0.]\n",
+ " [0. 0. 2.]]\n",
+ " diag:\n",
+ " [2. 2. 2.],\n",
+ " 'mu': array([0., 0., 0.]),\n",
+ " 'M': 3\n",
+ " }),\n",
+ " 'linear':\n",
+ " Constant matrix\n",
+ " [[1. 0. 0.]\n",
+ " [0. 1. 0.]\n",
+ " [0. 0. 1.]]\n",
+ " .linear at 0x119028e00>\n",
+ " }),\n",
+ " 'tseq': \n",
+ " \n",
+ " - K: 25025\n",
+ " - Ko: 1000\n",
+ " - T: 250.25\n",
+ " - BurnIn: 16.0\n",
+ " - dto: 0.25\n",
+ " - dt: 0.01,\n",
+ " 'X0': \n",
+ " GaussRV({\n",
+ " 'C': \n",
+ " M: 3\n",
+ " kind: 'diag'\n",
+ " trunc: 1.0\n",
+ " rk: 3\n",
+ " full:\n",
+ " [[2. 0. 0.]\n",
+ " [0. 2. 0.]\n",
+ " [0. 0. 2.]]\n",
+ " diag:\n",
+ " [2. 2. 2.],\n",
+ " 'mu': array([ 1.509, -1.531, 25.46 ]),\n",
+ " 'M': 3\n",
+ " }),\n",
+ " 'liveplotters': \n",
+ " [(1, ),\n",
+ " (1, .init at\n",
+ " 0x11903c720>), (1, .init\n",
+ " at 0x11903c5e0>)],\n",
+ " 'sectors': {},\n",
+ " 'name': 'Lorenz63/sakov2012.py'\n",
+ " })"
+ ]
+ },
+ "execution_count": 4,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "from dapper.mods.Lorenz63.sakov2012 import HMM\n",
+ "HMM # ⇒ printout (in notebooks)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "2ce9b72c",
+ "metadata": {},
+ "source": [
+ "### Simulate synthetic truth (xx) and noisy obs (yy)\n",
+ "A variable named `` conventionally refers to a *time series* of ``."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "c54bcca2",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "Truth & Obs: 100%|█████████████| 3000/3000 [00:00<00:00, 42197.63it/s]\n"
+ ]
+ }
+ ],
+ "source": [
+ "HMM.tseq.T = 30 # shorten experiment\n",
+ "xx, yy = HMM.simulate()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3c40799e",
+ "metadata": {},
+ "source": [
+ "### Specify a DA method\n",
+ "Here \"xp\" is short for \"experiment\" configuration."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "id": "b39ebe02",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "EnKF(upd_a='Sqrt', N=10, infl=1.02, rot=True, fnoise_treatm='Stoch')"
+ ]
+ },
+ "execution_count": 6,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# xp = da.OptInterp()\n",
+ "# xp = da.Var3D()\n",
+ "# xp = da.ExtKF(infl=90)\n",
+ "xp = da.EnKF(\"Sqrt\", N=10, infl=1.02, rot=True)\n",
+ "# xp = da.PartFilt(N=100, reg=2.4, NER=0.3)\n",
+ "xp # ⇒ printout (in notebooks)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "08f20322",
+ "metadata": {},
+ "source": [
+ "### Assimilate yy\n",
+ "Note that the assimilation \"knows\" the HMM,\n",
+ "but `xx` is only used for performance *assessment*."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "0a0ea6ff",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "EnKF: 100%|████████████████████| 3000/3000 [00:00<00:00, 14291.97it/s]\n"
+ ]
+ }
+ ],
+ "source": [
+ "xp.assimilate(HMM, xx, yy)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "18ff2a15",
+ "metadata": {},
+ "source": [
+ "### Average the time series\n",
+ "Computes a set of different statistics."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "id": "a7f6fb40",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# print(xp.stats) # ⇒ long printout\n",
+ "xp.stats.average_in_time()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "842ad66b",
+ "metadata": {},
+ "source": [
+ "Print some of these time-averages"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "id": "cc4e9955",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "rmse.a 1σ rmv.a 1σ \n",
+ "------------ -----------\n",
+ " 0.78 ±0.06 0.57 ±0.07\n"
+ ]
+ }
+ ],
+ "source": [
+ "# print(xp.avrgs) # ⇒ long printout\n",
+ "print(xp.avrgs.tabulate([\"rmse.a\", \"rmv.a\"]))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "f130c09b",
+ "metadata": {},
+ "source": [
+ "### Replay liveplotters"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "id": "a7ebf5e6",
+ "metadata": {
+ "scrolled": false
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Initializing liveplots...\n"
+ ]
+ },
+ {
+ "data": {
+ "application/javascript": [
+ "/* Put everything inside the global mpl namespace */\n",
+ "/* global mpl */\n",
+ "window.mpl = {};\n",
+ "\n",
+ "mpl.get_websocket_type = function () {\n",
+ " if (typeof WebSocket !== 'undefined') {\n",
+ " return WebSocket;\n",
+ " } else if (typeof MozWebSocket !== 'undefined') {\n",
+ " return MozWebSocket;\n",
+ " } else {\n",
+ " alert(\n",
+ " 'Your browser does not have WebSocket support. ' +\n",
+ " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n",
+ " 'Firefox 4 and 5 are also supported but you ' +\n",
+ " 'have to enable WebSockets in about:config.'\n",
+ " );\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure = function (figure_id, websocket, ondownload, parent_element) {\n",
+ " this.id = figure_id;\n",
+ "\n",
+ " this.ws = websocket;\n",
+ "\n",
+ " this.supports_binary = this.ws.binaryType !== undefined;\n",
+ "\n",
+ " if (!this.supports_binary) {\n",
+ " var warnings = document.getElementById('mpl-warnings');\n",
+ " if (warnings) {\n",
+ " warnings.style.display = 'block';\n",
+ " warnings.textContent =\n",
+ " 'This browser does not support binary websocket messages. ' +\n",
+ " 'Performance may be slow.';\n",
+ " }\n",
+ " }\n",
+ "\n",
+ " this.imageObj = new Image();\n",
+ "\n",
+ " this.context = undefined;\n",
+ " this.message = undefined;\n",
+ " this.canvas = undefined;\n",
+ " this.rubberband_canvas = undefined;\n",
+ " this.rubberband_context = undefined;\n",
+ " this.format_dropdown = undefined;\n",
+ "\n",
+ " this.image_mode = 'full';\n",
+ "\n",
+ " this.root = document.createElement('div');\n",
+ " this.root.setAttribute('style', 'display: inline-block');\n",
+ " this._root_extra_style(this.root);\n",
+ "\n",
+ " parent_element.appendChild(this.root);\n",
+ "\n",
+ " this._init_header(this);\n",
+ " this._init_canvas(this);\n",
+ " this._init_toolbar(this);\n",
+ "\n",
+ " var fig = this;\n",
+ "\n",
+ " this.waiting = false;\n",
+ "\n",
+ " this.ws.onopen = function () {\n",
+ " fig.send_message('supports_binary', { value: fig.supports_binary });\n",
+ " fig.send_message('send_image_mode', {});\n",
+ " if (fig.ratio !== 1) {\n",
+ " fig.send_message('set_device_pixel_ratio', {\n",
+ " device_pixel_ratio: fig.ratio,\n",
+ " });\n",
+ " }\n",
+ " fig.send_message('refresh', {});\n",
+ " };\n",
+ "\n",
+ " this.imageObj.onload = function () {\n",
+ " if (fig.image_mode === 'full') {\n",
+ " // Full images could contain transparency (where diff images\n",
+ " // almost always do), so we need to clear the canvas so that\n",
+ " // there is no ghosting.\n",
+ " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n",
+ " }\n",
+ " fig.context.drawImage(fig.imageObj, 0, 0);\n",
+ " };\n",
+ "\n",
+ " this.imageObj.onunload = function () {\n",
+ " fig.ws.close();\n",
+ " };\n",
+ "\n",
+ " this.ws.onmessage = this._make_on_message_function(this);\n",
+ "\n",
+ " this.ondownload = ondownload;\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._init_header = function () {\n",
+ " var titlebar = document.createElement('div');\n",
+ " titlebar.classList =\n",
+ " 'ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix';\n",
+ " var titletext = document.createElement('div');\n",
+ " titletext.classList = 'ui-dialog-title';\n",
+ " titletext.setAttribute(\n",
+ " 'style',\n",
+ " 'width: 100%; text-align: center; padding: 3px;'\n",
+ " );\n",
+ " titlebar.appendChild(titletext);\n",
+ " this.root.appendChild(titlebar);\n",
+ " this.header = titletext;\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._canvas_extra_style = function (_canvas_div) {};\n",
+ "\n",
+ "mpl.figure.prototype._root_extra_style = function (_canvas_div) {};\n",
+ "\n",
+ "mpl.figure.prototype._init_canvas = function () {\n",
+ " var fig = this;\n",
+ "\n",
+ " var canvas_div = (this.canvas_div = document.createElement('div'));\n",
+ " canvas_div.setAttribute('tabindex', '0');\n",
+ " canvas_div.setAttribute(\n",
+ " 'style',\n",
+ " 'border: 1px solid #ddd;' +\n",
+ " 'box-sizing: content-box;' +\n",
+ " 'clear: both;' +\n",
+ " 'min-height: 1px;' +\n",
+ " 'min-width: 1px;' +\n",
+ " 'outline: 0;' +\n",
+ " 'overflow: hidden;' +\n",
+ " 'position: relative;' +\n",
+ " 'resize: both;' +\n",
+ " 'z-index: 2;'\n",
+ " );\n",
+ "\n",
+ " function on_keyboard_event_closure(name) {\n",
+ " return function (event) {\n",
+ " return fig.key_event(event, name);\n",
+ " };\n",
+ " }\n",
+ "\n",
+ " canvas_div.addEventListener(\n",
+ " 'keydown',\n",
+ " on_keyboard_event_closure('key_press')\n",
+ " );\n",
+ " canvas_div.addEventListener(\n",
+ " 'keyup',\n",
+ " on_keyboard_event_closure('key_release')\n",
+ " );\n",
+ "\n",
+ " this._canvas_extra_style(canvas_div);\n",
+ " this.root.appendChild(canvas_div);\n",
+ "\n",
+ " var canvas = (this.canvas = document.createElement('canvas'));\n",
+ " canvas.classList.add('mpl-canvas');\n",
+ " canvas.setAttribute(\n",
+ " 'style',\n",
+ " 'box-sizing: content-box;' +\n",
+ " 'pointer-events: none;' +\n",
+ " 'position: relative;' +\n",
+ " 'z-index: 0;'\n",
+ " );\n",
+ "\n",
+ " this.context = canvas.getContext('2d');\n",
+ "\n",
+ " var backingStore =\n",
+ " this.context.backingStorePixelRatio ||\n",
+ " this.context.webkitBackingStorePixelRatio ||\n",
+ " this.context.mozBackingStorePixelRatio ||\n",
+ " this.context.msBackingStorePixelRatio ||\n",
+ " this.context.oBackingStorePixelRatio ||\n",
+ " this.context.backingStorePixelRatio ||\n",
+ " 1;\n",
+ "\n",
+ " this.ratio = (window.devicePixelRatio || 1) / backingStore;\n",
+ "\n",
+ " var rubberband_canvas = (this.rubberband_canvas = document.createElement(\n",
+ " 'canvas'\n",
+ " ));\n",
+ " rubberband_canvas.setAttribute(\n",
+ " 'style',\n",
+ " 'box-sizing: content-box;' +\n",
+ " 'left: 0;' +\n",
+ " 'pointer-events: none;' +\n",
+ " 'position: absolute;' +\n",
+ " 'top: 0;' +\n",
+ " 'z-index: 1;'\n",
+ " );\n",
+ "\n",
+ " // Apply a ponyfill if ResizeObserver is not implemented by browser.\n",
+ " if (this.ResizeObserver === undefined) {\n",
+ " if (window.ResizeObserver !== undefined) {\n",
+ " this.ResizeObserver = window.ResizeObserver;\n",
+ " } else {\n",
+ " var obs = _JSXTOOLS_RESIZE_OBSERVER({});\n",
+ " this.ResizeObserver = obs.ResizeObserver;\n",
+ " }\n",
+ " }\n",
+ "\n",
+ " this.resizeObserverInstance = new this.ResizeObserver(function (entries) {\n",
+ " var nentries = entries.length;\n",
+ " for (var i = 0; i < nentries; i++) {\n",
+ " var entry = entries[i];\n",
+ " var width, height;\n",
+ " if (entry.contentBoxSize) {\n",
+ " if (entry.contentBoxSize instanceof Array) {\n",
+ " // Chrome 84 implements new version of spec.\n",
+ " width = entry.contentBoxSize[0].inlineSize;\n",
+ " height = entry.contentBoxSize[0].blockSize;\n",
+ " } else {\n",
+ " // Firefox implements old version of spec.\n",
+ " width = entry.contentBoxSize.inlineSize;\n",
+ " height = entry.contentBoxSize.blockSize;\n",
+ " }\n",
+ " } else {\n",
+ " // Chrome <84 implements even older version of spec.\n",
+ " width = entry.contentRect.width;\n",
+ " height = entry.contentRect.height;\n",
+ " }\n",
+ "\n",
+ " // Keep the size of the canvas and rubber band canvas in sync with\n",
+ " // the canvas container.\n",
+ " if (entry.devicePixelContentBoxSize) {\n",
+ " // Chrome 84 implements new version of spec.\n",
+ " canvas.setAttribute(\n",
+ " 'width',\n",
+ " entry.devicePixelContentBoxSize[0].inlineSize\n",
+ " );\n",
+ " canvas.setAttribute(\n",
+ " 'height',\n",
+ " entry.devicePixelContentBoxSize[0].blockSize\n",
+ " );\n",
+ " } else {\n",
+ " canvas.setAttribute('width', width * fig.ratio);\n",
+ " canvas.setAttribute('height', height * fig.ratio);\n",
+ " }\n",
+ " /* This rescales the canvas back to display pixels, so that it\n",
+ " * appears correct on HiDPI screens. */\n",
+ " canvas.style.width = width + 'px';\n",
+ " canvas.style.height = height + 'px';\n",
+ "\n",
+ " rubberband_canvas.setAttribute('width', width);\n",
+ " rubberband_canvas.setAttribute('height', height);\n",
+ "\n",
+ " // And update the size in Python. We ignore the initial 0/0 size\n",
+ " // that occurs as the element is placed into the DOM, which should\n",
+ " // otherwise not happen due to the minimum size styling.\n",
+ " if (fig.ws.readyState == 1 && width != 0 && height != 0) {\n",
+ " fig.request_resize(width, height);\n",
+ " }\n",
+ " }\n",
+ " });\n",
+ " this.resizeObserverInstance.observe(canvas_div);\n",
+ "\n",
+ " function on_mouse_event_closure(name) {\n",
+ " /* User Agent sniffing is bad, but WebKit is busted:\n",
+ " * https://bugs.webkit.org/show_bug.cgi?id=144526\n",
+ " * https://bugs.webkit.org/show_bug.cgi?id=181818\n",
+ " * The worst that happens here is that they get an extra browser\n",
+ " * selection when dragging, if this check fails to catch them.\n",
+ " */\n",
+ " var UA = navigator.userAgent;\n",
+ " var isWebKit = /AppleWebKit/.test(UA) && !/Chrome/.test(UA);\n",
+ " if(isWebKit) {\n",
+ " return function (event) {\n",
+ " /* This prevents the web browser from automatically changing to\n",
+ " * the text insertion cursor when the button is pressed. We\n",
+ " * want to control all of the cursor setting manually through\n",
+ " * the 'cursor' event from matplotlib */\n",
+ " event.preventDefault()\n",
+ " return fig.mouse_event(event, name);\n",
+ " };\n",
+ " } else {\n",
+ " return function (event) {\n",
+ " return fig.mouse_event(event, name);\n",
+ " };\n",
+ " }\n",
+ " }\n",
+ "\n",
+ " canvas_div.addEventListener(\n",
+ " 'mousedown',\n",
+ " on_mouse_event_closure('button_press')\n",
+ " );\n",
+ " canvas_div.addEventListener(\n",
+ " 'mouseup',\n",
+ " on_mouse_event_closure('button_release')\n",
+ " );\n",
+ " canvas_div.addEventListener(\n",
+ " 'dblclick',\n",
+ " on_mouse_event_closure('dblclick')\n",
+ " );\n",
+ " // Throttle sequential mouse events to 1 every 20ms.\n",
+ " canvas_div.addEventListener(\n",
+ " 'mousemove',\n",
+ " on_mouse_event_closure('motion_notify')\n",
+ " );\n",
+ "\n",
+ " canvas_div.addEventListener(\n",
+ " 'mouseenter',\n",
+ " on_mouse_event_closure('figure_enter')\n",
+ " );\n",
+ " canvas_div.addEventListener(\n",
+ " 'mouseleave',\n",
+ " on_mouse_event_closure('figure_leave')\n",
+ " );\n",
+ "\n",
+ " canvas_div.addEventListener('wheel', function (event) {\n",
+ " if (event.deltaY < 0) {\n",
+ " event.step = 1;\n",
+ " } else {\n",
+ " event.step = -1;\n",
+ " }\n",
+ " on_mouse_event_closure('scroll')(event);\n",
+ " });\n",
+ "\n",
+ " canvas_div.appendChild(canvas);\n",
+ " canvas_div.appendChild(rubberband_canvas);\n",
+ "\n",
+ " this.rubberband_context = rubberband_canvas.getContext('2d');\n",
+ " this.rubberband_context.strokeStyle = '#000000';\n",
+ "\n",
+ " this._resize_canvas = function (width, height, forward) {\n",
+ " if (forward) {\n",
+ " canvas_div.style.width = width + 'px';\n",
+ " canvas_div.style.height = height + 'px';\n",
+ " }\n",
+ " };\n",
+ "\n",
+ " // Disable right mouse context menu.\n",
+ " canvas_div.addEventListener('contextmenu', function (_e) {\n",
+ " event.preventDefault();\n",
+ " return false;\n",
+ " });\n",
+ "\n",
+ " function set_focus() {\n",
+ " canvas.focus();\n",
+ " canvas_div.focus();\n",
+ " }\n",
+ "\n",
+ " window.setTimeout(set_focus, 100);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._init_toolbar = function () {\n",
+ " var fig = this;\n",
+ "\n",
+ " var toolbar = document.createElement('div');\n",
+ " toolbar.classList = 'mpl-toolbar';\n",
+ " this.root.appendChild(toolbar);\n",
+ "\n",
+ " function on_click_closure(name) {\n",
+ " return function (_event) {\n",
+ " return fig.toolbar_button_onclick(name);\n",
+ " };\n",
+ " }\n",
+ "\n",
+ " function on_mouseover_closure(tooltip) {\n",
+ " return function (event) {\n",
+ " if (!event.currentTarget.disabled) {\n",
+ " return fig.toolbar_button_onmouseover(tooltip);\n",
+ " }\n",
+ " };\n",
+ " }\n",
+ "\n",
+ " fig.buttons = {};\n",
+ " var buttonGroup = document.createElement('div');\n",
+ " buttonGroup.classList = 'mpl-button-group';\n",
+ " for (var toolbar_ind in mpl.toolbar_items) {\n",
+ " var name = mpl.toolbar_items[toolbar_ind][0];\n",
+ " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
+ " var image = mpl.toolbar_items[toolbar_ind][2];\n",
+ " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
+ "\n",
+ " if (!name) {\n",
+ " /* Instead of a spacer, we start a new button group. */\n",
+ " if (buttonGroup.hasChildNodes()) {\n",
+ " toolbar.appendChild(buttonGroup);\n",
+ " }\n",
+ " buttonGroup = document.createElement('div');\n",
+ " buttonGroup.classList = 'mpl-button-group';\n",
+ " continue;\n",
+ " }\n",
+ "\n",
+ " var button = (fig.buttons[name] = document.createElement('button'));\n",
+ " button.classList = 'mpl-widget';\n",
+ " button.setAttribute('role', 'button');\n",
+ " button.setAttribute('aria-disabled', 'false');\n",
+ " button.addEventListener('click', on_click_closure(method_name));\n",
+ " button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n",
+ "\n",
+ " var icon_img = document.createElement('img');\n",
+ " icon_img.src = '_images/' + image + '.png';\n",
+ " icon_img.srcset = '_images/' + image + '_large.png 2x';\n",
+ " icon_img.alt = tooltip;\n",
+ " button.appendChild(icon_img);\n",
+ "\n",
+ " buttonGroup.appendChild(button);\n",
+ " }\n",
+ "\n",
+ " if (buttonGroup.hasChildNodes()) {\n",
+ " toolbar.appendChild(buttonGroup);\n",
+ " }\n",
+ "\n",
+ " var fmt_picker = document.createElement('select');\n",
+ " fmt_picker.classList = 'mpl-widget';\n",
+ " toolbar.appendChild(fmt_picker);\n",
+ " this.format_dropdown = fmt_picker;\n",
+ "\n",
+ " for (var ind in mpl.extensions) {\n",
+ " var fmt = mpl.extensions[ind];\n",
+ " var option = document.createElement('option');\n",
+ " option.selected = fmt === mpl.default_extension;\n",
+ " option.innerHTML = fmt;\n",
+ " fmt_picker.appendChild(option);\n",
+ " }\n",
+ "\n",
+ " var status_bar = document.createElement('span');\n",
+ " status_bar.classList = 'mpl-message';\n",
+ " toolbar.appendChild(status_bar);\n",
+ " this.message = status_bar;\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.request_resize = function (x_pixels, y_pixels) {\n",
+ " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n",
+ " // which will in turn request a refresh of the image.\n",
+ " this.send_message('resize', { width: x_pixels, height: y_pixels });\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.send_message = function (type, properties) {\n",
+ " properties['type'] = type;\n",
+ " properties['figure_id'] = this.id;\n",
+ " this.ws.send(JSON.stringify(properties));\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.send_draw_message = function () {\n",
+ " if (!this.waiting) {\n",
+ " this.waiting = true;\n",
+ " this.ws.send(JSON.stringify({ type: 'draw', figure_id: this.id }));\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_save = function (fig, _msg) {\n",
+ " var format_dropdown = fig.format_dropdown;\n",
+ " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n",
+ " fig.ondownload(fig, format);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_resize = function (fig, msg) {\n",
+ " var size = msg['size'];\n",
+ " if (size[0] !== fig.canvas.width || size[1] !== fig.canvas.height) {\n",
+ " fig._resize_canvas(size[0], size[1], msg['forward']);\n",
+ " fig.send_message('refresh', {});\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_rubberband = function (fig, msg) {\n",
+ " var x0 = msg['x0'] / fig.ratio;\n",
+ " var y0 = (fig.canvas.height - msg['y0']) / fig.ratio;\n",
+ " var x1 = msg['x1'] / fig.ratio;\n",
+ " var y1 = (fig.canvas.height - msg['y1']) / fig.ratio;\n",
+ " x0 = Math.floor(x0) + 0.5;\n",
+ " y0 = Math.floor(y0) + 0.5;\n",
+ " x1 = Math.floor(x1) + 0.5;\n",
+ " y1 = Math.floor(y1) + 0.5;\n",
+ " var min_x = Math.min(x0, x1);\n",
+ " var min_y = Math.min(y0, y1);\n",
+ " var width = Math.abs(x1 - x0);\n",
+ " var height = Math.abs(y1 - y0);\n",
+ "\n",
+ " fig.rubberband_context.clearRect(\n",
+ " 0,\n",
+ " 0,\n",
+ " fig.canvas.width / fig.ratio,\n",
+ " fig.canvas.height / fig.ratio\n",
+ " );\n",
+ "\n",
+ " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_figure_label = function (fig, msg) {\n",
+ " // Updates the figure title.\n",
+ " fig.header.textContent = msg['label'];\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_cursor = function (fig, msg) {\n",
+ " fig.canvas_div.style.cursor = msg['cursor'];\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_message = function (fig, msg) {\n",
+ " fig.message.textContent = msg['message'];\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_draw = function (fig, _msg) {\n",
+ " // Request the server to send over a new figure.\n",
+ " fig.send_draw_message();\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_image_mode = function (fig, msg) {\n",
+ " fig.image_mode = msg['mode'];\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_history_buttons = function (fig, msg) {\n",
+ " for (var key in msg) {\n",
+ " if (!(key in fig.buttons)) {\n",
+ " continue;\n",
+ " }\n",
+ " fig.buttons[key].disabled = !msg[key];\n",
+ " fig.buttons[key].setAttribute('aria-disabled', !msg[key]);\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_navigate_mode = function (fig, msg) {\n",
+ " if (msg['mode'] === 'PAN') {\n",
+ " fig.buttons['Pan'].classList.add('active');\n",
+ " fig.buttons['Zoom'].classList.remove('active');\n",
+ " } else if (msg['mode'] === 'ZOOM') {\n",
+ " fig.buttons['Pan'].classList.remove('active');\n",
+ " fig.buttons['Zoom'].classList.add('active');\n",
+ " } else {\n",
+ " fig.buttons['Pan'].classList.remove('active');\n",
+ " fig.buttons['Zoom'].classList.remove('active');\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.updated_canvas_event = function () {\n",
+ " // Called whenever the canvas gets updated.\n",
+ " this.send_message('ack', {});\n",
+ "};\n",
+ "\n",
+ "// A function to construct a web socket function for onmessage handling.\n",
+ "// Called in the figure constructor.\n",
+ "mpl.figure.prototype._make_on_message_function = function (fig) {\n",
+ " return function socket_on_message(evt) {\n",
+ " if (evt.data instanceof Blob) {\n",
+ " var img = evt.data;\n",
+ " if (img.type !== 'image/png') {\n",
+ " /* FIXME: We get \"Resource interpreted as Image but\n",
+ " * transferred with MIME type text/plain:\" errors on\n",
+ " * Chrome. But how to set the MIME type? It doesn't seem\n",
+ " * to be part of the websocket stream */\n",
+ " img.type = 'image/png';\n",
+ " }\n",
+ "\n",
+ " /* Free the memory for the previous frames */\n",
+ " if (fig.imageObj.src) {\n",
+ " (window.URL || window.webkitURL).revokeObjectURL(\n",
+ " fig.imageObj.src\n",
+ " );\n",
+ " }\n",
+ "\n",
+ " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
+ " img\n",
+ " );\n",
+ " fig.updated_canvas_event();\n",
+ " fig.waiting = false;\n",
+ " return;\n",
+ " } else if (\n",
+ " typeof evt.data === 'string' &&\n",
+ " evt.data.slice(0, 21) === 'data:image/png;base64'\n",
+ " ) {\n",
+ " fig.imageObj.src = evt.data;\n",
+ " fig.updated_canvas_event();\n",
+ " fig.waiting = false;\n",
+ " return;\n",
+ " }\n",
+ "\n",
+ " var msg = JSON.parse(evt.data);\n",
+ " var msg_type = msg['type'];\n",
+ "\n",
+ " // Call the \"handle_{type}\" callback, which takes\n",
+ " // the figure and JSON message as its only arguments.\n",
+ " try {\n",
+ " var callback = fig['handle_' + msg_type];\n",
+ " } catch (e) {\n",
+ " console.log(\n",
+ " \"No handler for the '\" + msg_type + \"' message type: \",\n",
+ " msg\n",
+ " );\n",
+ " return;\n",
+ " }\n",
+ "\n",
+ " if (callback) {\n",
+ " try {\n",
+ " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n",
+ " callback(fig, msg);\n",
+ " } catch (e) {\n",
+ " console.log(\n",
+ " \"Exception inside the 'handler_\" + msg_type + \"' callback:\",\n",
+ " e,\n",
+ " e.stack,\n",
+ " msg\n",
+ " );\n",
+ " }\n",
+ " }\n",
+ " };\n",
+ "};\n",
+ "\n",
+ "function getModifiers(event) {\n",
+ " var mods = [];\n",
+ " if (event.ctrlKey) {\n",
+ " mods.push('ctrl');\n",
+ " }\n",
+ " if (event.altKey) {\n",
+ " mods.push('alt');\n",
+ " }\n",
+ " if (event.shiftKey) {\n",
+ " mods.push('shift');\n",
+ " }\n",
+ " if (event.metaKey) {\n",
+ " mods.push('meta');\n",
+ " }\n",
+ " return mods;\n",
+ "}\n",
+ "\n",
+ "/*\n",
+ " * return a copy of an object with only non-object keys\n",
+ " * we need this to avoid circular references\n",
+ " * https://stackoverflow.com/a/24161582/3208463\n",
+ " */\n",
+ "function simpleKeys(original) {\n",
+ " return Object.keys(original).reduce(function (obj, key) {\n",
+ " if (typeof original[key] !== 'object') {\n",
+ " obj[key] = original[key];\n",
+ " }\n",
+ " return obj;\n",
+ " }, {});\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.mouse_event = function (event, name) {\n",
+ " if (name === 'button_press') {\n",
+ " this.canvas.focus();\n",
+ " this.canvas_div.focus();\n",
+ " }\n",
+ "\n",
+ " // from https://stackoverflow.com/q/1114465\n",
+ " var boundingRect = this.canvas.getBoundingClientRect();\n",
+ " var x = (event.clientX - boundingRect.left) * this.ratio;\n",
+ " var y = (event.clientY - boundingRect.top) * this.ratio;\n",
+ "\n",
+ " this.send_message(name, {\n",
+ " x: x,\n",
+ " y: y,\n",
+ " button: event.button,\n",
+ " step: event.step,\n",
+ " modifiers: getModifiers(event),\n",
+ " guiEvent: simpleKeys(event),\n",
+ " });\n",
+ "\n",
+ " return false;\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._key_event_extra = function (_event, _name) {\n",
+ " // Handle any extra behaviour associated with a key event\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.key_event = function (event, name) {\n",
+ " // Prevent repeat events\n",
+ " if (name === 'key_press') {\n",
+ " if (event.key === this._key) {\n",
+ " return;\n",
+ " } else {\n",
+ " this._key = event.key;\n",
+ " }\n",
+ " }\n",
+ " if (name === 'key_release') {\n",
+ " this._key = null;\n",
+ " }\n",
+ "\n",
+ " var value = '';\n",
+ " if (event.ctrlKey && event.key !== 'Control') {\n",
+ " value += 'ctrl+';\n",
+ " }\n",
+ " else if (event.altKey && event.key !== 'Alt') {\n",
+ " value += 'alt+';\n",
+ " }\n",
+ " else if (event.shiftKey && event.key !== 'Shift') {\n",
+ " value += 'shift+';\n",
+ " }\n",
+ "\n",
+ " value += 'k' + event.key;\n",
+ "\n",
+ " this._key_event_extra(event, name);\n",
+ "\n",
+ " this.send_message(name, { key: value, guiEvent: simpleKeys(event) });\n",
+ " return false;\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.toolbar_button_onclick = function (name) {\n",
+ " if (name === 'download') {\n",
+ " this.handle_save(this, null);\n",
+ " } else {\n",
+ " this.send_message('toolbar_button', { name: name });\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.toolbar_button_onmouseover = function (tooltip) {\n",
+ " this.message.textContent = tooltip;\n",
+ "};\n",
+ "\n",
+ "///////////////// REMAINING CONTENT GENERATED BY embed_js.py /////////////////\n",
+ "// prettier-ignore\n",
+ "var _JSXTOOLS_RESIZE_OBSERVER=function(A){var t,i=new WeakMap,n=new WeakMap,a=new WeakMap,r=new WeakMap,o=new Set;function s(e){if(!(this instanceof s))throw new TypeError(\"Constructor requires 'new' operator\");i.set(this,e)}function h(){throw new TypeError(\"Function is not a constructor\")}function c(e,t,i,n){e=0 in arguments?Number(arguments[0]):0,t=1 in arguments?Number(arguments[1]):0,i=2 in arguments?Number(arguments[2]):0,n=3 in arguments?Number(arguments[3]):0,this.right=(this.x=this.left=e)+(this.width=i),this.bottom=(this.y=this.top=t)+(this.height=n),Object.freeze(this)}function d(){t=requestAnimationFrame(d);var s=new WeakMap,p=new Set;o.forEach((function(t){r.get(t).forEach((function(i){var r=t instanceof window.SVGElement,o=a.get(t),d=r?0:parseFloat(o.paddingTop),f=r?0:parseFloat(o.paddingRight),l=r?0:parseFloat(o.paddingBottom),u=r?0:parseFloat(o.paddingLeft),g=r?0:parseFloat(o.borderTopWidth),m=r?0:parseFloat(o.borderRightWidth),w=r?0:parseFloat(o.borderBottomWidth),b=u+f,F=d+l,v=(r?0:parseFloat(o.borderLeftWidth))+m,W=g+w,y=r?0:t.offsetHeight-W-t.clientHeight,E=r?0:t.offsetWidth-v-t.clientWidth,R=b+v,z=F+W,M=r?t.width:parseFloat(o.width)-R-E,O=r?t.height:parseFloat(o.height)-z-y;if(n.has(t)){var k=n.get(t);if(k[0]===M&&k[1]===O)return}n.set(t,[M,O]);var S=Object.create(h.prototype);S.target=t,S.contentRect=new c(u,d,M,O),s.has(i)||(s.set(i,[]),p.add(i)),s.get(i).push(S)}))})),p.forEach((function(e){i.get(e).call(e,s.get(e),e)}))}return s.prototype.observe=function(i){if(i instanceof window.Element){r.has(i)||(r.set(i,new Set),o.add(i),a.set(i,window.getComputedStyle(i)));var n=r.get(i);n.has(this)||n.add(this),cancelAnimationFrame(t),t=requestAnimationFrame(d)}},s.prototype.unobserve=function(i){if(i instanceof window.Element&&r.has(i)){var n=r.get(i);n.has(this)&&(n.delete(this),n.size||(r.delete(i),o.delete(i))),n.size||r.delete(i),o.size||cancelAnimationFrame(t)}},A.DOMRectReadOnly=c,A.ResizeObserver=s,A.ResizeObserverEntry=h,A}; // eslint-disable-line\n",
+ "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Left button pans, Right button zooms\\nx/y fixes axis, CTRL fixes aspect\", \"fa fa-arrows\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\\nx/y fixes axis\", \"fa fa-square-o\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o\", \"download\"]];\n",
+ "\n",
+ "mpl.extensions = [\"eps\", \"jpeg\", \"pgf\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\", \"tif\", \"webp\"];\n",
+ "\n",
+ "mpl.default_extension = \"png\";/* global mpl */\n",
+ "\n",
+ "var comm_websocket_adapter = function (comm) {\n",
+ " // Create a \"websocket\"-like object which calls the given IPython comm\n",
+ " // object with the appropriate methods. Currently this is a non binary\n",
+ " // socket, so there is still some room for performance tuning.\n",
+ " var ws = {};\n",
+ "\n",
+ " ws.binaryType = comm.kernel.ws.binaryType;\n",
+ " ws.readyState = comm.kernel.ws.readyState;\n",
+ " function updateReadyState(_event) {\n",
+ " if (comm.kernel.ws) {\n",
+ " ws.readyState = comm.kernel.ws.readyState;\n",
+ " } else {\n",
+ " ws.readyState = 3; // Closed state.\n",
+ " }\n",
+ " }\n",
+ " comm.kernel.ws.addEventListener('open', updateReadyState);\n",
+ " comm.kernel.ws.addEventListener('close', updateReadyState);\n",
+ " comm.kernel.ws.addEventListener('error', updateReadyState);\n",
+ "\n",
+ " ws.close = function () {\n",
+ " comm.close();\n",
+ " };\n",
+ " ws.send = function (m) {\n",
+ " //console.log('sending', m);\n",
+ " comm.send(m);\n",
+ " };\n",
+ " // Register the callback with on_msg.\n",
+ " comm.on_msg(function (msg) {\n",
+ " //console.log('receiving', msg['content']['data'], msg);\n",
+ " var data = msg['content']['data'];\n",
+ " if (data['blob'] !== undefined) {\n",
+ " data = {\n",
+ " data: new Blob(msg['buffers'], { type: data['blob'] }),\n",
+ " };\n",
+ " }\n",
+ " // Pass the mpl event to the overridden (by mpl) onmessage function.\n",
+ " ws.onmessage(data);\n",
+ " });\n",
+ " return ws;\n",
+ "};\n",
+ "\n",
+ "mpl.mpl_figure_comm = function (comm, msg) {\n",
+ " // This is the function which gets called when the mpl process\n",
+ " // starts-up an IPython Comm through the \"matplotlib\" channel.\n",
+ "\n",
+ " var id = msg.content.data.id;\n",
+ " // Get hold of the div created by the display call when the Comm\n",
+ " // socket was opened in Python.\n",
+ " var element = document.getElementById(id);\n",
+ " var ws_proxy = comm_websocket_adapter(comm);\n",
+ "\n",
+ " function ondownload(figure, _format) {\n",
+ " window.open(figure.canvas.toDataURL());\n",
+ " }\n",
+ "\n",
+ " var fig = new mpl.figure(id, ws_proxy, ondownload, element);\n",
+ "\n",
+ " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n",
+ " // web socket which is closed, not our websocket->open comm proxy.\n",
+ " ws_proxy.onopen();\n",
+ "\n",
+ " fig.parent_element = element;\n",
+ " fig.cell_info = mpl.find_output_cell(\"\");\n",
+ " if (!fig.cell_info) {\n",
+ " console.error('Failed to find cell for figure', id, fig);\n",
+ " return;\n",
+ " }\n",
+ " fig.cell_info[0].output_area.element.on(\n",
+ " 'cleared',\n",
+ " { fig: fig },\n",
+ " fig._remove_fig_handler\n",
+ " );\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_close = function (fig, msg) {\n",
+ " var width = fig.canvas.width / fig.ratio;\n",
+ " fig.cell_info[0].output_area.element.off(\n",
+ " 'cleared',\n",
+ " fig._remove_fig_handler\n",
+ " );\n",
+ " fig.resizeObserverInstance.unobserve(fig.canvas_div);\n",
+ "\n",
+ " // Update the output cell to use the data from the current canvas.\n",
+ " fig.push_to_output();\n",
+ " var dataURL = fig.canvas.toDataURL();\n",
+ " // Re-enable the keyboard manager in IPython - without this line, in FF,\n",
+ " // the notebook keyboard shortcuts fail.\n",
+ " IPython.keyboard_manager.enable();\n",
+ " fig.parent_element.innerHTML =\n",
+ " '';\n",
+ " fig.close_ws(fig, msg);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.close_ws = function (fig, msg) {\n",
+ " fig.send_message('closing', msg);\n",
+ " // fig.ws.close()\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.push_to_output = function (_remove_interactive) {\n",
+ " // Turn the data on the canvas into data in the output cell.\n",
+ " var width = this.canvas.width / this.ratio;\n",
+ " var dataURL = this.canvas.toDataURL();\n",
+ " this.cell_info[1]['text/html'] =\n",
+ " '';\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.updated_canvas_event = function () {\n",
+ " // Tell IPython that the notebook contents must change.\n",
+ " IPython.notebook.set_dirty(true);\n",
+ " this.send_message('ack', {});\n",
+ " var fig = this;\n",
+ " // Wait a second, then push the new image to the DOM so\n",
+ " // that it is saved nicely (might be nice to debounce this).\n",
+ " setTimeout(function () {\n",
+ " fig.push_to_output();\n",
+ " }, 1000);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._init_toolbar = function () {\n",
+ " var fig = this;\n",
+ "\n",
+ " var toolbar = document.createElement('div');\n",
+ " toolbar.classList = 'btn-toolbar';\n",
+ " this.root.appendChild(toolbar);\n",
+ "\n",
+ " function on_click_closure(name) {\n",
+ " return function (_event) {\n",
+ " return fig.toolbar_button_onclick(name);\n",
+ " };\n",
+ " }\n",
+ "\n",
+ " function on_mouseover_closure(tooltip) {\n",
+ " return function (event) {\n",
+ " if (!event.currentTarget.disabled) {\n",
+ " return fig.toolbar_button_onmouseover(tooltip);\n",
+ " }\n",
+ " };\n",
+ " }\n",
+ "\n",
+ " fig.buttons = {};\n",
+ " var buttonGroup = document.createElement('div');\n",
+ " buttonGroup.classList = 'btn-group';\n",
+ " var button;\n",
+ " for (var toolbar_ind in mpl.toolbar_items) {\n",
+ " var name = mpl.toolbar_items[toolbar_ind][0];\n",
+ " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
+ " var image = mpl.toolbar_items[toolbar_ind][2];\n",
+ " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
+ "\n",
+ " if (!name) {\n",
+ " /* Instead of a spacer, we start a new button group. */\n",
+ " if (buttonGroup.hasChildNodes()) {\n",
+ " toolbar.appendChild(buttonGroup);\n",
+ " }\n",
+ " buttonGroup = document.createElement('div');\n",
+ " buttonGroup.classList = 'btn-group';\n",
+ " continue;\n",
+ " }\n",
+ "\n",
+ " button = fig.buttons[name] = document.createElement('button');\n",
+ " button.classList = 'btn btn-default';\n",
+ " button.href = '#';\n",
+ " button.title = name;\n",
+ " button.innerHTML = '';\n",
+ " button.addEventListener('click', on_click_closure(method_name));\n",
+ " button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n",
+ " buttonGroup.appendChild(button);\n",
+ " }\n",
+ "\n",
+ " if (buttonGroup.hasChildNodes()) {\n",
+ " toolbar.appendChild(buttonGroup);\n",
+ " }\n",
+ "\n",
+ " // Add the status bar.\n",
+ " var status_bar = document.createElement('span');\n",
+ " status_bar.classList = 'mpl-message pull-right';\n",
+ " toolbar.appendChild(status_bar);\n",
+ " this.message = status_bar;\n",
+ "\n",
+ " // Add the close button to the window.\n",
+ " var buttongrp = document.createElement('div');\n",
+ " buttongrp.classList = 'btn-group inline pull-right';\n",
+ " button = document.createElement('button');\n",
+ " button.classList = 'btn btn-mini btn-primary';\n",
+ " button.href = '#';\n",
+ " button.title = 'Stop Interaction';\n",
+ " button.innerHTML = '';\n",
+ " button.addEventListener('click', function (_evt) {\n",
+ " fig.handle_close(fig, {});\n",
+ " });\n",
+ " button.addEventListener(\n",
+ " 'mouseover',\n",
+ " on_mouseover_closure('Stop Interaction')\n",
+ " );\n",
+ " buttongrp.appendChild(button);\n",
+ " var titlebar = this.root.querySelector('.ui-dialog-titlebar');\n",
+ " titlebar.insertBefore(buttongrp, titlebar.firstChild);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._remove_fig_handler = function (event) {\n",
+ " var fig = event.data.fig;\n",
+ " if (event.target !== this) {\n",
+ " // Ignore bubbled events from children.\n",
+ " return;\n",
+ " }\n",
+ " fig.close_ws(fig, {});\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._root_extra_style = function (el) {\n",
+ " el.style.boxSizing = 'content-box'; // override notebook setting of border-box.\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._canvas_extra_style = function (el) {\n",
+ " // this is important to make the div 'focusable\n",
+ " el.setAttribute('tabindex', 0);\n",
+ " // reach out to IPython and tell the keyboard manager to turn it's self\n",
+ " // off when our div gets focus\n",
+ "\n",
+ " // location in version 3\n",
+ " if (IPython.notebook.keyboard_manager) {\n",
+ " IPython.notebook.keyboard_manager.register_events(el);\n",
+ " } else {\n",
+ " // location in version 2\n",
+ " IPython.keyboard_manager.register_events(el);\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._key_event_extra = function (event, _name) {\n",
+ " // Check for shift+enter\n",
+ " if (event.shiftKey && event.which === 13) {\n",
+ " this.canvas_div.blur();\n",
+ " // select the cell after this one\n",
+ " var index = IPython.notebook.find_cell_index(this.cell_info[0]);\n",
+ " IPython.notebook.select(index + 1);\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_save = function (fig, _msg) {\n",
+ " fig.ondownload(fig, null);\n",
+ "};\n",
+ "\n",
+ "mpl.find_output_cell = function (html_output) {\n",
+ " // Return the cell and output element which can be found *uniquely* in the notebook.\n",
+ " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n",
+ " // IPython event is triggered only after the cells have been serialised, which for\n",
+ " // our purposes (turning an active figure into a static one), is too late.\n",
+ " var cells = IPython.notebook.get_cells();\n",
+ " var ncells = cells.length;\n",
+ " for (var i = 0; i < ncells; i++) {\n",
+ " var cell = cells[i];\n",
+ " if (cell.cell_type === 'code') {\n",
+ " for (var j = 0; j < cell.output_area.outputs.length; j++) {\n",
+ " var data = cell.output_area.outputs[j];\n",
+ " if (data.data) {\n",
+ " // IPython >= 3 moved mimebundle to data attribute of output\n",
+ " data = data.data;\n",
+ " }\n",
+ " if (data['text/html'] === html_output) {\n",
+ " return [cell, data, j];\n",
+ " }\n",
+ " }\n",
+ " }\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "// Register the function which deals with the matplotlib target/channel.\n",
+ "// The kernel may be null if the page has been refreshed.\n",
+ "if (IPython.notebook.kernel !== null) {\n",
+ " IPython.notebook.kernel.comm_manager.register_target(\n",
+ " 'matplotlib',\n",
+ " mpl.mpl_figure_comm\n",
+ " );\n",
+ "}\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/html": [
+ ""
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/javascript": [
+ "/* Put everything inside the global mpl namespace */\n",
+ "/* global mpl */\n",
+ "window.mpl = {};\n",
+ "\n",
+ "mpl.get_websocket_type = function () {\n",
+ " if (typeof WebSocket !== 'undefined') {\n",
+ " return WebSocket;\n",
+ " } else if (typeof MozWebSocket !== 'undefined') {\n",
+ " return MozWebSocket;\n",
+ " } else {\n",
+ " alert(\n",
+ " 'Your browser does not have WebSocket support. ' +\n",
+ " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n",
+ " 'Firefox 4 and 5 are also supported but you ' +\n",
+ " 'have to enable WebSockets in about:config.'\n",
+ " );\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure = function (figure_id, websocket, ondownload, parent_element) {\n",
+ " this.id = figure_id;\n",
+ "\n",
+ " this.ws = websocket;\n",
+ "\n",
+ " this.supports_binary = this.ws.binaryType !== undefined;\n",
+ "\n",
+ " if (!this.supports_binary) {\n",
+ " var warnings = document.getElementById('mpl-warnings');\n",
+ " if (warnings) {\n",
+ " warnings.style.display = 'block';\n",
+ " warnings.textContent =\n",
+ " 'This browser does not support binary websocket messages. ' +\n",
+ " 'Performance may be slow.';\n",
+ " }\n",
+ " }\n",
+ "\n",
+ " this.imageObj = new Image();\n",
+ "\n",
+ " this.context = undefined;\n",
+ " this.message = undefined;\n",
+ " this.canvas = undefined;\n",
+ " this.rubberband_canvas = undefined;\n",
+ " this.rubberband_context = undefined;\n",
+ " this.format_dropdown = undefined;\n",
+ "\n",
+ " this.image_mode = 'full';\n",
+ "\n",
+ " this.root = document.createElement('div');\n",
+ " this.root.setAttribute('style', 'display: inline-block');\n",
+ " this._root_extra_style(this.root);\n",
+ "\n",
+ " parent_element.appendChild(this.root);\n",
+ "\n",
+ " this._init_header(this);\n",
+ " this._init_canvas(this);\n",
+ " this._init_toolbar(this);\n",
+ "\n",
+ " var fig = this;\n",
+ "\n",
+ " this.waiting = false;\n",
+ "\n",
+ " this.ws.onopen = function () {\n",
+ " fig.send_message('supports_binary', { value: fig.supports_binary });\n",
+ " fig.send_message('send_image_mode', {});\n",
+ " if (fig.ratio !== 1) {\n",
+ " fig.send_message('set_device_pixel_ratio', {\n",
+ " device_pixel_ratio: fig.ratio,\n",
+ " });\n",
+ " }\n",
+ " fig.send_message('refresh', {});\n",
+ " };\n",
+ "\n",
+ " this.imageObj.onload = function () {\n",
+ " if (fig.image_mode === 'full') {\n",
+ " // Full images could contain transparency (where diff images\n",
+ " // almost always do), so we need to clear the canvas so that\n",
+ " // there is no ghosting.\n",
+ " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n",
+ " }\n",
+ " fig.context.drawImage(fig.imageObj, 0, 0);\n",
+ " };\n",
+ "\n",
+ " this.imageObj.onunload = function () {\n",
+ " fig.ws.close();\n",
+ " };\n",
+ "\n",
+ " this.ws.onmessage = this._make_on_message_function(this);\n",
+ "\n",
+ " this.ondownload = ondownload;\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._init_header = function () {\n",
+ " var titlebar = document.createElement('div');\n",
+ " titlebar.classList =\n",
+ " 'ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix';\n",
+ " var titletext = document.createElement('div');\n",
+ " titletext.classList = 'ui-dialog-title';\n",
+ " titletext.setAttribute(\n",
+ " 'style',\n",
+ " 'width: 100%; text-align: center; padding: 3px;'\n",
+ " );\n",
+ " titlebar.appendChild(titletext);\n",
+ " this.root.appendChild(titlebar);\n",
+ " this.header = titletext;\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._canvas_extra_style = function (_canvas_div) {};\n",
+ "\n",
+ "mpl.figure.prototype._root_extra_style = function (_canvas_div) {};\n",
+ "\n",
+ "mpl.figure.prototype._init_canvas = function () {\n",
+ " var fig = this;\n",
+ "\n",
+ " var canvas_div = (this.canvas_div = document.createElement('div'));\n",
+ " canvas_div.setAttribute('tabindex', '0');\n",
+ " canvas_div.setAttribute(\n",
+ " 'style',\n",
+ " 'border: 1px solid #ddd;' +\n",
+ " 'box-sizing: content-box;' +\n",
+ " 'clear: both;' +\n",
+ " 'min-height: 1px;' +\n",
+ " 'min-width: 1px;' +\n",
+ " 'outline: 0;' +\n",
+ " 'overflow: hidden;' +\n",
+ " 'position: relative;' +\n",
+ " 'resize: both;' +\n",
+ " 'z-index: 2;'\n",
+ " );\n",
+ "\n",
+ " function on_keyboard_event_closure(name) {\n",
+ " return function (event) {\n",
+ " return fig.key_event(event, name);\n",
+ " };\n",
+ " }\n",
+ "\n",
+ " canvas_div.addEventListener(\n",
+ " 'keydown',\n",
+ " on_keyboard_event_closure('key_press')\n",
+ " );\n",
+ " canvas_div.addEventListener(\n",
+ " 'keyup',\n",
+ " on_keyboard_event_closure('key_release')\n",
+ " );\n",
+ "\n",
+ " this._canvas_extra_style(canvas_div);\n",
+ " this.root.appendChild(canvas_div);\n",
+ "\n",
+ " var canvas = (this.canvas = document.createElement('canvas'));\n",
+ " canvas.classList.add('mpl-canvas');\n",
+ " canvas.setAttribute(\n",
+ " 'style',\n",
+ " 'box-sizing: content-box;' +\n",
+ " 'pointer-events: none;' +\n",
+ " 'position: relative;' +\n",
+ " 'z-index: 0;'\n",
+ " );\n",
+ "\n",
+ " this.context = canvas.getContext('2d');\n",
+ "\n",
+ " var backingStore =\n",
+ " this.context.backingStorePixelRatio ||\n",
+ " this.context.webkitBackingStorePixelRatio ||\n",
+ " this.context.mozBackingStorePixelRatio ||\n",
+ " this.context.msBackingStorePixelRatio ||\n",
+ " this.context.oBackingStorePixelRatio ||\n",
+ " this.context.backingStorePixelRatio ||\n",
+ " 1;\n",
+ "\n",
+ " this.ratio = (window.devicePixelRatio || 1) / backingStore;\n",
+ "\n",
+ " var rubberband_canvas = (this.rubberband_canvas = document.createElement(\n",
+ " 'canvas'\n",
+ " ));\n",
+ " rubberband_canvas.setAttribute(\n",
+ " 'style',\n",
+ " 'box-sizing: content-box;' +\n",
+ " 'left: 0;' +\n",
+ " 'pointer-events: none;' +\n",
+ " 'position: absolute;' +\n",
+ " 'top: 0;' +\n",
+ " 'z-index: 1;'\n",
+ " );\n",
+ "\n",
+ " // Apply a ponyfill if ResizeObserver is not implemented by browser.\n",
+ " if (this.ResizeObserver === undefined) {\n",
+ " if (window.ResizeObserver !== undefined) {\n",
+ " this.ResizeObserver = window.ResizeObserver;\n",
+ " } else {\n",
+ " var obs = _JSXTOOLS_RESIZE_OBSERVER({});\n",
+ " this.ResizeObserver = obs.ResizeObserver;\n",
+ " }\n",
+ " }\n",
+ "\n",
+ " this.resizeObserverInstance = new this.ResizeObserver(function (entries) {\n",
+ " var nentries = entries.length;\n",
+ " for (var i = 0; i < nentries; i++) {\n",
+ " var entry = entries[i];\n",
+ " var width, height;\n",
+ " if (entry.contentBoxSize) {\n",
+ " if (entry.contentBoxSize instanceof Array) {\n",
+ " // Chrome 84 implements new version of spec.\n",
+ " width = entry.contentBoxSize[0].inlineSize;\n",
+ " height = entry.contentBoxSize[0].blockSize;\n",
+ " } else {\n",
+ " // Firefox implements old version of spec.\n",
+ " width = entry.contentBoxSize.inlineSize;\n",
+ " height = entry.contentBoxSize.blockSize;\n",
+ " }\n",
+ " } else {\n",
+ " // Chrome <84 implements even older version of spec.\n",
+ " width = entry.contentRect.width;\n",
+ " height = entry.contentRect.height;\n",
+ " }\n",
+ "\n",
+ " // Keep the size of the canvas and rubber band canvas in sync with\n",
+ " // the canvas container.\n",
+ " if (entry.devicePixelContentBoxSize) {\n",
+ " // Chrome 84 implements new version of spec.\n",
+ " canvas.setAttribute(\n",
+ " 'width',\n",
+ " entry.devicePixelContentBoxSize[0].inlineSize\n",
+ " );\n",
+ " canvas.setAttribute(\n",
+ " 'height',\n",
+ " entry.devicePixelContentBoxSize[0].blockSize\n",
+ " );\n",
+ " } else {\n",
+ " canvas.setAttribute('width', width * fig.ratio);\n",
+ " canvas.setAttribute('height', height * fig.ratio);\n",
+ " }\n",
+ " /* This rescales the canvas back to display pixels, so that it\n",
+ " * appears correct on HiDPI screens. */\n",
+ " canvas.style.width = width + 'px';\n",
+ " canvas.style.height = height + 'px';\n",
+ "\n",
+ " rubberband_canvas.setAttribute('width', width);\n",
+ " rubberband_canvas.setAttribute('height', height);\n",
+ "\n",
+ " // And update the size in Python. We ignore the initial 0/0 size\n",
+ " // that occurs as the element is placed into the DOM, which should\n",
+ " // otherwise not happen due to the minimum size styling.\n",
+ " if (fig.ws.readyState == 1 && width != 0 && height != 0) {\n",
+ " fig.request_resize(width, height);\n",
+ " }\n",
+ " }\n",
+ " });\n",
+ " this.resizeObserverInstance.observe(canvas_div);\n",
+ "\n",
+ " function on_mouse_event_closure(name) {\n",
+ " /* User Agent sniffing is bad, but WebKit is busted:\n",
+ " * https://bugs.webkit.org/show_bug.cgi?id=144526\n",
+ " * https://bugs.webkit.org/show_bug.cgi?id=181818\n",
+ " * The worst that happens here is that they get an extra browser\n",
+ " * selection when dragging, if this check fails to catch them.\n",
+ " */\n",
+ " var UA = navigator.userAgent;\n",
+ " var isWebKit = /AppleWebKit/.test(UA) && !/Chrome/.test(UA);\n",
+ " if(isWebKit) {\n",
+ " return function (event) {\n",
+ " /* This prevents the web browser from automatically changing to\n",
+ " * the text insertion cursor when the button is pressed. We\n",
+ " * want to control all of the cursor setting manually through\n",
+ " * the 'cursor' event from matplotlib */\n",
+ " event.preventDefault()\n",
+ " return fig.mouse_event(event, name);\n",
+ " };\n",
+ " } else {\n",
+ " return function (event) {\n",
+ " return fig.mouse_event(event, name);\n",
+ " };\n",
+ " }\n",
+ " }\n",
+ "\n",
+ " canvas_div.addEventListener(\n",
+ " 'mousedown',\n",
+ " on_mouse_event_closure('button_press')\n",
+ " );\n",
+ " canvas_div.addEventListener(\n",
+ " 'mouseup',\n",
+ " on_mouse_event_closure('button_release')\n",
+ " );\n",
+ " canvas_div.addEventListener(\n",
+ " 'dblclick',\n",
+ " on_mouse_event_closure('dblclick')\n",
+ " );\n",
+ " // Throttle sequential mouse events to 1 every 20ms.\n",
+ " canvas_div.addEventListener(\n",
+ " 'mousemove',\n",
+ " on_mouse_event_closure('motion_notify')\n",
+ " );\n",
+ "\n",
+ " canvas_div.addEventListener(\n",
+ " 'mouseenter',\n",
+ " on_mouse_event_closure('figure_enter')\n",
+ " );\n",
+ " canvas_div.addEventListener(\n",
+ " 'mouseleave',\n",
+ " on_mouse_event_closure('figure_leave')\n",
+ " );\n",
+ "\n",
+ " canvas_div.addEventListener('wheel', function (event) {\n",
+ " if (event.deltaY < 0) {\n",
+ " event.step = 1;\n",
+ " } else {\n",
+ " event.step = -1;\n",
+ " }\n",
+ " on_mouse_event_closure('scroll')(event);\n",
+ " });\n",
+ "\n",
+ " canvas_div.appendChild(canvas);\n",
+ " canvas_div.appendChild(rubberband_canvas);\n",
+ "\n",
+ " this.rubberband_context = rubberband_canvas.getContext('2d');\n",
+ " this.rubberband_context.strokeStyle = '#000000';\n",
+ "\n",
+ " this._resize_canvas = function (width, height, forward) {\n",
+ " if (forward) {\n",
+ " canvas_div.style.width = width + 'px';\n",
+ " canvas_div.style.height = height + 'px';\n",
+ " }\n",
+ " };\n",
+ "\n",
+ " // Disable right mouse context menu.\n",
+ " canvas_div.addEventListener('contextmenu', function (_e) {\n",
+ " event.preventDefault();\n",
+ " return false;\n",
+ " });\n",
+ "\n",
+ " function set_focus() {\n",
+ " canvas.focus();\n",
+ " canvas_div.focus();\n",
+ " }\n",
+ "\n",
+ " window.setTimeout(set_focus, 100);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._init_toolbar = function () {\n",
+ " var fig = this;\n",
+ "\n",
+ " var toolbar = document.createElement('div');\n",
+ " toolbar.classList = 'mpl-toolbar';\n",
+ " this.root.appendChild(toolbar);\n",
+ "\n",
+ " function on_click_closure(name) {\n",
+ " return function (_event) {\n",
+ " return fig.toolbar_button_onclick(name);\n",
+ " };\n",
+ " }\n",
+ "\n",
+ " function on_mouseover_closure(tooltip) {\n",
+ " return function (event) {\n",
+ " if (!event.currentTarget.disabled) {\n",
+ " return fig.toolbar_button_onmouseover(tooltip);\n",
+ " }\n",
+ " };\n",
+ " }\n",
+ "\n",
+ " fig.buttons = {};\n",
+ " var buttonGroup = document.createElement('div');\n",
+ " buttonGroup.classList = 'mpl-button-group';\n",
+ " for (var toolbar_ind in mpl.toolbar_items) {\n",
+ " var name = mpl.toolbar_items[toolbar_ind][0];\n",
+ " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
+ " var image = mpl.toolbar_items[toolbar_ind][2];\n",
+ " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
+ "\n",
+ " if (!name) {\n",
+ " /* Instead of a spacer, we start a new button group. */\n",
+ " if (buttonGroup.hasChildNodes()) {\n",
+ " toolbar.appendChild(buttonGroup);\n",
+ " }\n",
+ " buttonGroup = document.createElement('div');\n",
+ " buttonGroup.classList = 'mpl-button-group';\n",
+ " continue;\n",
+ " }\n",
+ "\n",
+ " var button = (fig.buttons[name] = document.createElement('button'));\n",
+ " button.classList = 'mpl-widget';\n",
+ " button.setAttribute('role', 'button');\n",
+ " button.setAttribute('aria-disabled', 'false');\n",
+ " button.addEventListener('click', on_click_closure(method_name));\n",
+ " button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n",
+ "\n",
+ " var icon_img = document.createElement('img');\n",
+ " icon_img.src = '_images/' + image + '.png';\n",
+ " icon_img.srcset = '_images/' + image + '_large.png 2x';\n",
+ " icon_img.alt = tooltip;\n",
+ " button.appendChild(icon_img);\n",
+ "\n",
+ " buttonGroup.appendChild(button);\n",
+ " }\n",
+ "\n",
+ " if (buttonGroup.hasChildNodes()) {\n",
+ " toolbar.appendChild(buttonGroup);\n",
+ " }\n",
+ "\n",
+ " var fmt_picker = document.createElement('select');\n",
+ " fmt_picker.classList = 'mpl-widget';\n",
+ " toolbar.appendChild(fmt_picker);\n",
+ " this.format_dropdown = fmt_picker;\n",
+ "\n",
+ " for (var ind in mpl.extensions) {\n",
+ " var fmt = mpl.extensions[ind];\n",
+ " var option = document.createElement('option');\n",
+ " option.selected = fmt === mpl.default_extension;\n",
+ " option.innerHTML = fmt;\n",
+ " fmt_picker.appendChild(option);\n",
+ " }\n",
+ "\n",
+ " var status_bar = document.createElement('span');\n",
+ " status_bar.classList = 'mpl-message';\n",
+ " toolbar.appendChild(status_bar);\n",
+ " this.message = status_bar;\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.request_resize = function (x_pixels, y_pixels) {\n",
+ " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n",
+ " // which will in turn request a refresh of the image.\n",
+ " this.send_message('resize', { width: x_pixels, height: y_pixels });\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.send_message = function (type, properties) {\n",
+ " properties['type'] = type;\n",
+ " properties['figure_id'] = this.id;\n",
+ " this.ws.send(JSON.stringify(properties));\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.send_draw_message = function () {\n",
+ " if (!this.waiting) {\n",
+ " this.waiting = true;\n",
+ " this.ws.send(JSON.stringify({ type: 'draw', figure_id: this.id }));\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_save = function (fig, _msg) {\n",
+ " var format_dropdown = fig.format_dropdown;\n",
+ " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n",
+ " fig.ondownload(fig, format);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_resize = function (fig, msg) {\n",
+ " var size = msg['size'];\n",
+ " if (size[0] !== fig.canvas.width || size[1] !== fig.canvas.height) {\n",
+ " fig._resize_canvas(size[0], size[1], msg['forward']);\n",
+ " fig.send_message('refresh', {});\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_rubberband = function (fig, msg) {\n",
+ " var x0 = msg['x0'] / fig.ratio;\n",
+ " var y0 = (fig.canvas.height - msg['y0']) / fig.ratio;\n",
+ " var x1 = msg['x1'] / fig.ratio;\n",
+ " var y1 = (fig.canvas.height - msg['y1']) / fig.ratio;\n",
+ " x0 = Math.floor(x0) + 0.5;\n",
+ " y0 = Math.floor(y0) + 0.5;\n",
+ " x1 = Math.floor(x1) + 0.5;\n",
+ " y1 = Math.floor(y1) + 0.5;\n",
+ " var min_x = Math.min(x0, x1);\n",
+ " var min_y = Math.min(y0, y1);\n",
+ " var width = Math.abs(x1 - x0);\n",
+ " var height = Math.abs(y1 - y0);\n",
+ "\n",
+ " fig.rubberband_context.clearRect(\n",
+ " 0,\n",
+ " 0,\n",
+ " fig.canvas.width / fig.ratio,\n",
+ " fig.canvas.height / fig.ratio\n",
+ " );\n",
+ "\n",
+ " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_figure_label = function (fig, msg) {\n",
+ " // Updates the figure title.\n",
+ " fig.header.textContent = msg['label'];\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_cursor = function (fig, msg) {\n",
+ " fig.canvas_div.style.cursor = msg['cursor'];\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_message = function (fig, msg) {\n",
+ " fig.message.textContent = msg['message'];\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_draw = function (fig, _msg) {\n",
+ " // Request the server to send over a new figure.\n",
+ " fig.send_draw_message();\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_image_mode = function (fig, msg) {\n",
+ " fig.image_mode = msg['mode'];\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_history_buttons = function (fig, msg) {\n",
+ " for (var key in msg) {\n",
+ " if (!(key in fig.buttons)) {\n",
+ " continue;\n",
+ " }\n",
+ " fig.buttons[key].disabled = !msg[key];\n",
+ " fig.buttons[key].setAttribute('aria-disabled', !msg[key]);\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_navigate_mode = function (fig, msg) {\n",
+ " if (msg['mode'] === 'PAN') {\n",
+ " fig.buttons['Pan'].classList.add('active');\n",
+ " fig.buttons['Zoom'].classList.remove('active');\n",
+ " } else if (msg['mode'] === 'ZOOM') {\n",
+ " fig.buttons['Pan'].classList.remove('active');\n",
+ " fig.buttons['Zoom'].classList.add('active');\n",
+ " } else {\n",
+ " fig.buttons['Pan'].classList.remove('active');\n",
+ " fig.buttons['Zoom'].classList.remove('active');\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.updated_canvas_event = function () {\n",
+ " // Called whenever the canvas gets updated.\n",
+ " this.send_message('ack', {});\n",
+ "};\n",
+ "\n",
+ "// A function to construct a web socket function for onmessage handling.\n",
+ "// Called in the figure constructor.\n",
+ "mpl.figure.prototype._make_on_message_function = function (fig) {\n",
+ " return function socket_on_message(evt) {\n",
+ " if (evt.data instanceof Blob) {\n",
+ " var img = evt.data;\n",
+ " if (img.type !== 'image/png') {\n",
+ " /* FIXME: We get \"Resource interpreted as Image but\n",
+ " * transferred with MIME type text/plain:\" errors on\n",
+ " * Chrome. But how to set the MIME type? It doesn't seem\n",
+ " * to be part of the websocket stream */\n",
+ " img.type = 'image/png';\n",
+ " }\n",
+ "\n",
+ " /* Free the memory for the previous frames */\n",
+ " if (fig.imageObj.src) {\n",
+ " (window.URL || window.webkitURL).revokeObjectURL(\n",
+ " fig.imageObj.src\n",
+ " );\n",
+ " }\n",
+ "\n",
+ " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
+ " img\n",
+ " );\n",
+ " fig.updated_canvas_event();\n",
+ " fig.waiting = false;\n",
+ " return;\n",
+ " } else if (\n",
+ " typeof evt.data === 'string' &&\n",
+ " evt.data.slice(0, 21) === 'data:image/png;base64'\n",
+ " ) {\n",
+ " fig.imageObj.src = evt.data;\n",
+ " fig.updated_canvas_event();\n",
+ " fig.waiting = false;\n",
+ " return;\n",
+ " }\n",
+ "\n",
+ " var msg = JSON.parse(evt.data);\n",
+ " var msg_type = msg['type'];\n",
+ "\n",
+ " // Call the \"handle_{type}\" callback, which takes\n",
+ " // the figure and JSON message as its only arguments.\n",
+ " try {\n",
+ " var callback = fig['handle_' + msg_type];\n",
+ " } catch (e) {\n",
+ " console.log(\n",
+ " \"No handler for the '\" + msg_type + \"' message type: \",\n",
+ " msg\n",
+ " );\n",
+ " return;\n",
+ " }\n",
+ "\n",
+ " if (callback) {\n",
+ " try {\n",
+ " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n",
+ " callback(fig, msg);\n",
+ " } catch (e) {\n",
+ " console.log(\n",
+ " \"Exception inside the 'handler_\" + msg_type + \"' callback:\",\n",
+ " e,\n",
+ " e.stack,\n",
+ " msg\n",
+ " );\n",
+ " }\n",
+ " }\n",
+ " };\n",
+ "};\n",
+ "\n",
+ "function getModifiers(event) {\n",
+ " var mods = [];\n",
+ " if (event.ctrlKey) {\n",
+ " mods.push('ctrl');\n",
+ " }\n",
+ " if (event.altKey) {\n",
+ " mods.push('alt');\n",
+ " }\n",
+ " if (event.shiftKey) {\n",
+ " mods.push('shift');\n",
+ " }\n",
+ " if (event.metaKey) {\n",
+ " mods.push('meta');\n",
+ " }\n",
+ " return mods;\n",
+ "}\n",
+ "\n",
+ "/*\n",
+ " * return a copy of an object with only non-object keys\n",
+ " * we need this to avoid circular references\n",
+ " * https://stackoverflow.com/a/24161582/3208463\n",
+ " */\n",
+ "function simpleKeys(original) {\n",
+ " return Object.keys(original).reduce(function (obj, key) {\n",
+ " if (typeof original[key] !== 'object') {\n",
+ " obj[key] = original[key];\n",
+ " }\n",
+ " return obj;\n",
+ " }, {});\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.mouse_event = function (event, name) {\n",
+ " if (name === 'button_press') {\n",
+ " this.canvas.focus();\n",
+ " this.canvas_div.focus();\n",
+ " }\n",
+ "\n",
+ " // from https://stackoverflow.com/q/1114465\n",
+ " var boundingRect = this.canvas.getBoundingClientRect();\n",
+ " var x = (event.clientX - boundingRect.left) * this.ratio;\n",
+ " var y = (event.clientY - boundingRect.top) * this.ratio;\n",
+ "\n",
+ " this.send_message(name, {\n",
+ " x: x,\n",
+ " y: y,\n",
+ " button: event.button,\n",
+ " step: event.step,\n",
+ " modifiers: getModifiers(event),\n",
+ " guiEvent: simpleKeys(event),\n",
+ " });\n",
+ "\n",
+ " return false;\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._key_event_extra = function (_event, _name) {\n",
+ " // Handle any extra behaviour associated with a key event\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.key_event = function (event, name) {\n",
+ " // Prevent repeat events\n",
+ " if (name === 'key_press') {\n",
+ " if (event.key === this._key) {\n",
+ " return;\n",
+ " } else {\n",
+ " this._key = event.key;\n",
+ " }\n",
+ " }\n",
+ " if (name === 'key_release') {\n",
+ " this._key = null;\n",
+ " }\n",
+ "\n",
+ " var value = '';\n",
+ " if (event.ctrlKey && event.key !== 'Control') {\n",
+ " value += 'ctrl+';\n",
+ " }\n",
+ " else if (event.altKey && event.key !== 'Alt') {\n",
+ " value += 'alt+';\n",
+ " }\n",
+ " else if (event.shiftKey && event.key !== 'Shift') {\n",
+ " value += 'shift+';\n",
+ " }\n",
+ "\n",
+ " value += 'k' + event.key;\n",
+ "\n",
+ " this._key_event_extra(event, name);\n",
+ "\n",
+ " this.send_message(name, { key: value, guiEvent: simpleKeys(event) });\n",
+ " return false;\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.toolbar_button_onclick = function (name) {\n",
+ " if (name === 'download') {\n",
+ " this.handle_save(this, null);\n",
+ " } else {\n",
+ " this.send_message('toolbar_button', { name: name });\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.toolbar_button_onmouseover = function (tooltip) {\n",
+ " this.message.textContent = tooltip;\n",
+ "};\n",
+ "\n",
+ "///////////////// REMAINING CONTENT GENERATED BY embed_js.py /////////////////\n",
+ "// prettier-ignore\n",
+ "var _JSXTOOLS_RESIZE_OBSERVER=function(A){var t,i=new WeakMap,n=new WeakMap,a=new WeakMap,r=new WeakMap,o=new Set;function s(e){if(!(this instanceof s))throw new TypeError(\"Constructor requires 'new' operator\");i.set(this,e)}function h(){throw new TypeError(\"Function is not a constructor\")}function c(e,t,i,n){e=0 in arguments?Number(arguments[0]):0,t=1 in arguments?Number(arguments[1]):0,i=2 in arguments?Number(arguments[2]):0,n=3 in arguments?Number(arguments[3]):0,this.right=(this.x=this.left=e)+(this.width=i),this.bottom=(this.y=this.top=t)+(this.height=n),Object.freeze(this)}function d(){t=requestAnimationFrame(d);var s=new WeakMap,p=new Set;o.forEach((function(t){r.get(t).forEach((function(i){var r=t instanceof window.SVGElement,o=a.get(t),d=r?0:parseFloat(o.paddingTop),f=r?0:parseFloat(o.paddingRight),l=r?0:parseFloat(o.paddingBottom),u=r?0:parseFloat(o.paddingLeft),g=r?0:parseFloat(o.borderTopWidth),m=r?0:parseFloat(o.borderRightWidth),w=r?0:parseFloat(o.borderBottomWidth),b=u+f,F=d+l,v=(r?0:parseFloat(o.borderLeftWidth))+m,W=g+w,y=r?0:t.offsetHeight-W-t.clientHeight,E=r?0:t.offsetWidth-v-t.clientWidth,R=b+v,z=F+W,M=r?t.width:parseFloat(o.width)-R-E,O=r?t.height:parseFloat(o.height)-z-y;if(n.has(t)){var k=n.get(t);if(k[0]===M&&k[1]===O)return}n.set(t,[M,O]);var S=Object.create(h.prototype);S.target=t,S.contentRect=new c(u,d,M,O),s.has(i)||(s.set(i,[]),p.add(i)),s.get(i).push(S)}))})),p.forEach((function(e){i.get(e).call(e,s.get(e),e)}))}return s.prototype.observe=function(i){if(i instanceof window.Element){r.has(i)||(r.set(i,new Set),o.add(i),a.set(i,window.getComputedStyle(i)));var n=r.get(i);n.has(this)||n.add(this),cancelAnimationFrame(t),t=requestAnimationFrame(d)}},s.prototype.unobserve=function(i){if(i instanceof window.Element&&r.has(i)){var n=r.get(i);n.has(this)&&(n.delete(this),n.size||(r.delete(i),o.delete(i))),n.size||r.delete(i),o.size||cancelAnimationFrame(t)}},A.DOMRectReadOnly=c,A.ResizeObserver=s,A.ResizeObserverEntry=h,A}; // eslint-disable-line\n",
+ "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Left button pans, Right button zooms\\nx/y fixes axis, CTRL fixes aspect\", \"fa fa-arrows\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\\nx/y fixes axis\", \"fa fa-square-o\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o\", \"download\"]];\n",
+ "\n",
+ "mpl.extensions = [\"eps\", \"jpeg\", \"pgf\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\", \"tif\", \"webp\"];\n",
+ "\n",
+ "mpl.default_extension = \"png\";/* global mpl */\n",
+ "\n",
+ "var comm_websocket_adapter = function (comm) {\n",
+ " // Create a \"websocket\"-like object which calls the given IPython comm\n",
+ " // object with the appropriate methods. Currently this is a non binary\n",
+ " // socket, so there is still some room for performance tuning.\n",
+ " var ws = {};\n",
+ "\n",
+ " ws.binaryType = comm.kernel.ws.binaryType;\n",
+ " ws.readyState = comm.kernel.ws.readyState;\n",
+ " function updateReadyState(_event) {\n",
+ " if (comm.kernel.ws) {\n",
+ " ws.readyState = comm.kernel.ws.readyState;\n",
+ " } else {\n",
+ " ws.readyState = 3; // Closed state.\n",
+ " }\n",
+ " }\n",
+ " comm.kernel.ws.addEventListener('open', updateReadyState);\n",
+ " comm.kernel.ws.addEventListener('close', updateReadyState);\n",
+ " comm.kernel.ws.addEventListener('error', updateReadyState);\n",
+ "\n",
+ " ws.close = function () {\n",
+ " comm.close();\n",
+ " };\n",
+ " ws.send = function (m) {\n",
+ " //console.log('sending', m);\n",
+ " comm.send(m);\n",
+ " };\n",
+ " // Register the callback with on_msg.\n",
+ " comm.on_msg(function (msg) {\n",
+ " //console.log('receiving', msg['content']['data'], msg);\n",
+ " var data = msg['content']['data'];\n",
+ " if (data['blob'] !== undefined) {\n",
+ " data = {\n",
+ " data: new Blob(msg['buffers'], { type: data['blob'] }),\n",
+ " };\n",
+ " }\n",
+ " // Pass the mpl event to the overridden (by mpl) onmessage function.\n",
+ " ws.onmessage(data);\n",
+ " });\n",
+ " return ws;\n",
+ "};\n",
+ "\n",
+ "mpl.mpl_figure_comm = function (comm, msg) {\n",
+ " // This is the function which gets called when the mpl process\n",
+ " // starts-up an IPython Comm through the \"matplotlib\" channel.\n",
+ "\n",
+ " var id = msg.content.data.id;\n",
+ " // Get hold of the div created by the display call when the Comm\n",
+ " // socket was opened in Python.\n",
+ " var element = document.getElementById(id);\n",
+ " var ws_proxy = comm_websocket_adapter(comm);\n",
+ "\n",
+ " function ondownload(figure, _format) {\n",
+ " window.open(figure.canvas.toDataURL());\n",
+ " }\n",
+ "\n",
+ " var fig = new mpl.figure(id, ws_proxy, ondownload, element);\n",
+ "\n",
+ " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n",
+ " // web socket which is closed, not our websocket->open comm proxy.\n",
+ " ws_proxy.onopen();\n",
+ "\n",
+ " fig.parent_element = element;\n",
+ " fig.cell_info = mpl.find_output_cell(\"\");\n",
+ " if (!fig.cell_info) {\n",
+ " console.error('Failed to find cell for figure', id, fig);\n",
+ " return;\n",
+ " }\n",
+ " fig.cell_info[0].output_area.element.on(\n",
+ " 'cleared',\n",
+ " { fig: fig },\n",
+ " fig._remove_fig_handler\n",
+ " );\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_close = function (fig, msg) {\n",
+ " var width = fig.canvas.width / fig.ratio;\n",
+ " fig.cell_info[0].output_area.element.off(\n",
+ " 'cleared',\n",
+ " fig._remove_fig_handler\n",
+ " );\n",
+ " fig.resizeObserverInstance.unobserve(fig.canvas_div);\n",
+ "\n",
+ " // Update the output cell to use the data from the current canvas.\n",
+ " fig.push_to_output();\n",
+ " var dataURL = fig.canvas.toDataURL();\n",
+ " // Re-enable the keyboard manager in IPython - without this line, in FF,\n",
+ " // the notebook keyboard shortcuts fail.\n",
+ " IPython.keyboard_manager.enable();\n",
+ " fig.parent_element.innerHTML =\n",
+ " '';\n",
+ " fig.close_ws(fig, msg);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.close_ws = function (fig, msg) {\n",
+ " fig.send_message('closing', msg);\n",
+ " // fig.ws.close()\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.push_to_output = function (_remove_interactive) {\n",
+ " // Turn the data on the canvas into data in the output cell.\n",
+ " var width = this.canvas.width / this.ratio;\n",
+ " var dataURL = this.canvas.toDataURL();\n",
+ " this.cell_info[1]['text/html'] =\n",
+ " '';\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.updated_canvas_event = function () {\n",
+ " // Tell IPython that the notebook contents must change.\n",
+ " IPython.notebook.set_dirty(true);\n",
+ " this.send_message('ack', {});\n",
+ " var fig = this;\n",
+ " // Wait a second, then push the new image to the DOM so\n",
+ " // that it is saved nicely (might be nice to debounce this).\n",
+ " setTimeout(function () {\n",
+ " fig.push_to_output();\n",
+ " }, 1000);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._init_toolbar = function () {\n",
+ " var fig = this;\n",
+ "\n",
+ " var toolbar = document.createElement('div');\n",
+ " toolbar.classList = 'btn-toolbar';\n",
+ " this.root.appendChild(toolbar);\n",
+ "\n",
+ " function on_click_closure(name) {\n",
+ " return function (_event) {\n",
+ " return fig.toolbar_button_onclick(name);\n",
+ " };\n",
+ " }\n",
+ "\n",
+ " function on_mouseover_closure(tooltip) {\n",
+ " return function (event) {\n",
+ " if (!event.currentTarget.disabled) {\n",
+ " return fig.toolbar_button_onmouseover(tooltip);\n",
+ " }\n",
+ " };\n",
+ " }\n",
+ "\n",
+ " fig.buttons = {};\n",
+ " var buttonGroup = document.createElement('div');\n",
+ " buttonGroup.classList = 'btn-group';\n",
+ " var button;\n",
+ " for (var toolbar_ind in mpl.toolbar_items) {\n",
+ " var name = mpl.toolbar_items[toolbar_ind][0];\n",
+ " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
+ " var image = mpl.toolbar_items[toolbar_ind][2];\n",
+ " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
+ "\n",
+ " if (!name) {\n",
+ " /* Instead of a spacer, we start a new button group. */\n",
+ " if (buttonGroup.hasChildNodes()) {\n",
+ " toolbar.appendChild(buttonGroup);\n",
+ " }\n",
+ " buttonGroup = document.createElement('div');\n",
+ " buttonGroup.classList = 'btn-group';\n",
+ " continue;\n",
+ " }\n",
+ "\n",
+ " button = fig.buttons[name] = document.createElement('button');\n",
+ " button.classList = 'btn btn-default';\n",
+ " button.href = '#';\n",
+ " button.title = name;\n",
+ " button.innerHTML = '';\n",
+ " button.addEventListener('click', on_click_closure(method_name));\n",
+ " button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n",
+ " buttonGroup.appendChild(button);\n",
+ " }\n",
+ "\n",
+ " if (buttonGroup.hasChildNodes()) {\n",
+ " toolbar.appendChild(buttonGroup);\n",
+ " }\n",
+ "\n",
+ " // Add the status bar.\n",
+ " var status_bar = document.createElement('span');\n",
+ " status_bar.classList = 'mpl-message pull-right';\n",
+ " toolbar.appendChild(status_bar);\n",
+ " this.message = status_bar;\n",
+ "\n",
+ " // Add the close button to the window.\n",
+ " var buttongrp = document.createElement('div');\n",
+ " buttongrp.classList = 'btn-group inline pull-right';\n",
+ " button = document.createElement('button');\n",
+ " button.classList = 'btn btn-mini btn-primary';\n",
+ " button.href = '#';\n",
+ " button.title = 'Stop Interaction';\n",
+ " button.innerHTML = '';\n",
+ " button.addEventListener('click', function (_evt) {\n",
+ " fig.handle_close(fig, {});\n",
+ " });\n",
+ " button.addEventListener(\n",
+ " 'mouseover',\n",
+ " on_mouseover_closure('Stop Interaction')\n",
+ " );\n",
+ " buttongrp.appendChild(button);\n",
+ " var titlebar = this.root.querySelector('.ui-dialog-titlebar');\n",
+ " titlebar.insertBefore(buttongrp, titlebar.firstChild);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._remove_fig_handler = function (event) {\n",
+ " var fig = event.data.fig;\n",
+ " if (event.target !== this) {\n",
+ " // Ignore bubbled events from children.\n",
+ " return;\n",
+ " }\n",
+ " fig.close_ws(fig, {});\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._root_extra_style = function (el) {\n",
+ " el.style.boxSizing = 'content-box'; // override notebook setting of border-box.\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._canvas_extra_style = function (el) {\n",
+ " // this is important to make the div 'focusable\n",
+ " el.setAttribute('tabindex', 0);\n",
+ " // reach out to IPython and tell the keyboard manager to turn it's self\n",
+ " // off when our div gets focus\n",
+ "\n",
+ " // location in version 3\n",
+ " if (IPython.notebook.keyboard_manager) {\n",
+ " IPython.notebook.keyboard_manager.register_events(el);\n",
+ " } else {\n",
+ " // location in version 2\n",
+ " IPython.keyboard_manager.register_events(el);\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._key_event_extra = function (event, _name) {\n",
+ " // Check for shift+enter\n",
+ " if (event.shiftKey && event.which === 13) {\n",
+ " this.canvas_div.blur();\n",
+ " // select the cell after this one\n",
+ " var index = IPython.notebook.find_cell_index(this.cell_info[0]);\n",
+ " IPython.notebook.select(index + 1);\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_save = function (fig, _msg) {\n",
+ " fig.ondownload(fig, null);\n",
+ "};\n",
+ "\n",
+ "mpl.find_output_cell = function (html_output) {\n",
+ " // Return the cell and output element which can be found *uniquely* in the notebook.\n",
+ " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n",
+ " // IPython event is triggered only after the cells have been serialised, which for\n",
+ " // our purposes (turning an active figure into a static one), is too late.\n",
+ " var cells = IPython.notebook.get_cells();\n",
+ " var ncells = cells.length;\n",
+ " for (var i = 0; i < ncells; i++) {\n",
+ " var cell = cells[i];\n",
+ " if (cell.cell_type === 'code') {\n",
+ " for (var j = 0; j < cell.output_area.outputs.length; j++) {\n",
+ " var data = cell.output_area.outputs[j];\n",
+ " if (data.data) {\n",
+ " // IPython >= 3 moved mimebundle to data attribute of output\n",
+ " data = data.data;\n",
+ " }\n",
+ " if (data['text/html'] === html_output) {\n",
+ " return [cell, data, j];\n",
+ " }\n",
+ " }\n",
+ " }\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "// Register the function which deals with the matplotlib target/channel.\n",
+ "// The kernel may be null if the page has been refreshed.\n",
+ "if (IPython.notebook.kernel !== null) {\n",
+ " IPython.notebook.kernel.comm_manager.register_target(\n",
+ " 'matplotlib',\n",
+ " mpl.mpl_figure_comm\n",
+ " );\n",
+ "}\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/html": [
+ ""
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/javascript": [
+ "/* Put everything inside the global mpl namespace */\n",
+ "/* global mpl */\n",
+ "window.mpl = {};\n",
+ "\n",
+ "mpl.get_websocket_type = function () {\n",
+ " if (typeof WebSocket !== 'undefined') {\n",
+ " return WebSocket;\n",
+ " } else if (typeof MozWebSocket !== 'undefined') {\n",
+ " return MozWebSocket;\n",
+ " } else {\n",
+ " alert(\n",
+ " 'Your browser does not have WebSocket support. ' +\n",
+ " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n",
+ " 'Firefox 4 and 5 are also supported but you ' +\n",
+ " 'have to enable WebSockets in about:config.'\n",
+ " );\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure = function (figure_id, websocket, ondownload, parent_element) {\n",
+ " this.id = figure_id;\n",
+ "\n",
+ " this.ws = websocket;\n",
+ "\n",
+ " this.supports_binary = this.ws.binaryType !== undefined;\n",
+ "\n",
+ " if (!this.supports_binary) {\n",
+ " var warnings = document.getElementById('mpl-warnings');\n",
+ " if (warnings) {\n",
+ " warnings.style.display = 'block';\n",
+ " warnings.textContent =\n",
+ " 'This browser does not support binary websocket messages. ' +\n",
+ " 'Performance may be slow.';\n",
+ " }\n",
+ " }\n",
+ "\n",
+ " this.imageObj = new Image();\n",
+ "\n",
+ " this.context = undefined;\n",
+ " this.message = undefined;\n",
+ " this.canvas = undefined;\n",
+ " this.rubberband_canvas = undefined;\n",
+ " this.rubberband_context = undefined;\n",
+ " this.format_dropdown = undefined;\n",
+ "\n",
+ " this.image_mode = 'full';\n",
+ "\n",
+ " this.root = document.createElement('div');\n",
+ " this.root.setAttribute('style', 'display: inline-block');\n",
+ " this._root_extra_style(this.root);\n",
+ "\n",
+ " parent_element.appendChild(this.root);\n",
+ "\n",
+ " this._init_header(this);\n",
+ " this._init_canvas(this);\n",
+ " this._init_toolbar(this);\n",
+ "\n",
+ " var fig = this;\n",
+ "\n",
+ " this.waiting = false;\n",
+ "\n",
+ " this.ws.onopen = function () {\n",
+ " fig.send_message('supports_binary', { value: fig.supports_binary });\n",
+ " fig.send_message('send_image_mode', {});\n",
+ " if (fig.ratio !== 1) {\n",
+ " fig.send_message('set_device_pixel_ratio', {\n",
+ " device_pixel_ratio: fig.ratio,\n",
+ " });\n",
+ " }\n",
+ " fig.send_message('refresh', {});\n",
+ " };\n",
+ "\n",
+ " this.imageObj.onload = function () {\n",
+ " if (fig.image_mode === 'full') {\n",
+ " // Full images could contain transparency (where diff images\n",
+ " // almost always do), so we need to clear the canvas so that\n",
+ " // there is no ghosting.\n",
+ " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n",
+ " }\n",
+ " fig.context.drawImage(fig.imageObj, 0, 0);\n",
+ " };\n",
+ "\n",
+ " this.imageObj.onunload = function () {\n",
+ " fig.ws.close();\n",
+ " };\n",
+ "\n",
+ " this.ws.onmessage = this._make_on_message_function(this);\n",
+ "\n",
+ " this.ondownload = ondownload;\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._init_header = function () {\n",
+ " var titlebar = document.createElement('div');\n",
+ " titlebar.classList =\n",
+ " 'ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix';\n",
+ " var titletext = document.createElement('div');\n",
+ " titletext.classList = 'ui-dialog-title';\n",
+ " titletext.setAttribute(\n",
+ " 'style',\n",
+ " 'width: 100%; text-align: center; padding: 3px;'\n",
+ " );\n",
+ " titlebar.appendChild(titletext);\n",
+ " this.root.appendChild(titlebar);\n",
+ " this.header = titletext;\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._canvas_extra_style = function (_canvas_div) {};\n",
+ "\n",
+ "mpl.figure.prototype._root_extra_style = function (_canvas_div) {};\n",
+ "\n",
+ "mpl.figure.prototype._init_canvas = function () {\n",
+ " var fig = this;\n",
+ "\n",
+ " var canvas_div = (this.canvas_div = document.createElement('div'));\n",
+ " canvas_div.setAttribute('tabindex', '0');\n",
+ " canvas_div.setAttribute(\n",
+ " 'style',\n",
+ " 'border: 1px solid #ddd;' +\n",
+ " 'box-sizing: content-box;' +\n",
+ " 'clear: both;' +\n",
+ " 'min-height: 1px;' +\n",
+ " 'min-width: 1px;' +\n",
+ " 'outline: 0;' +\n",
+ " 'overflow: hidden;' +\n",
+ " 'position: relative;' +\n",
+ " 'resize: both;' +\n",
+ " 'z-index: 2;'\n",
+ " );\n",
+ "\n",
+ " function on_keyboard_event_closure(name) {\n",
+ " return function (event) {\n",
+ " return fig.key_event(event, name);\n",
+ " };\n",
+ " }\n",
+ "\n",
+ " canvas_div.addEventListener(\n",
+ " 'keydown',\n",
+ " on_keyboard_event_closure('key_press')\n",
+ " );\n",
+ " canvas_div.addEventListener(\n",
+ " 'keyup',\n",
+ " on_keyboard_event_closure('key_release')\n",
+ " );\n",
+ "\n",
+ " this._canvas_extra_style(canvas_div);\n",
+ " this.root.appendChild(canvas_div);\n",
+ "\n",
+ " var canvas = (this.canvas = document.createElement('canvas'));\n",
+ " canvas.classList.add('mpl-canvas');\n",
+ " canvas.setAttribute(\n",
+ " 'style',\n",
+ " 'box-sizing: content-box;' +\n",
+ " 'pointer-events: none;' +\n",
+ " 'position: relative;' +\n",
+ " 'z-index: 0;'\n",
+ " );\n",
+ "\n",
+ " this.context = canvas.getContext('2d');\n",
+ "\n",
+ " var backingStore =\n",
+ " this.context.backingStorePixelRatio ||\n",
+ " this.context.webkitBackingStorePixelRatio ||\n",
+ " this.context.mozBackingStorePixelRatio ||\n",
+ " this.context.msBackingStorePixelRatio ||\n",
+ " this.context.oBackingStorePixelRatio ||\n",
+ " this.context.backingStorePixelRatio ||\n",
+ " 1;\n",
+ "\n",
+ " this.ratio = (window.devicePixelRatio || 1) / backingStore;\n",
+ "\n",
+ " var rubberband_canvas = (this.rubberband_canvas = document.createElement(\n",
+ " 'canvas'\n",
+ " ));\n",
+ " rubberband_canvas.setAttribute(\n",
+ " 'style',\n",
+ " 'box-sizing: content-box;' +\n",
+ " 'left: 0;' +\n",
+ " 'pointer-events: none;' +\n",
+ " 'position: absolute;' +\n",
+ " 'top: 0;' +\n",
+ " 'z-index: 1;'\n",
+ " );\n",
+ "\n",
+ " // Apply a ponyfill if ResizeObserver is not implemented by browser.\n",
+ " if (this.ResizeObserver === undefined) {\n",
+ " if (window.ResizeObserver !== undefined) {\n",
+ " this.ResizeObserver = window.ResizeObserver;\n",
+ " } else {\n",
+ " var obs = _JSXTOOLS_RESIZE_OBSERVER({});\n",
+ " this.ResizeObserver = obs.ResizeObserver;\n",
+ " }\n",
+ " }\n",
+ "\n",
+ " this.resizeObserverInstance = new this.ResizeObserver(function (entries) {\n",
+ " var nentries = entries.length;\n",
+ " for (var i = 0; i < nentries; i++) {\n",
+ " var entry = entries[i];\n",
+ " var width, height;\n",
+ " if (entry.contentBoxSize) {\n",
+ " if (entry.contentBoxSize instanceof Array) {\n",
+ " // Chrome 84 implements new version of spec.\n",
+ " width = entry.contentBoxSize[0].inlineSize;\n",
+ " height = entry.contentBoxSize[0].blockSize;\n",
+ " } else {\n",
+ " // Firefox implements old version of spec.\n",
+ " width = entry.contentBoxSize.inlineSize;\n",
+ " height = entry.contentBoxSize.blockSize;\n",
+ " }\n",
+ " } else {\n",
+ " // Chrome <84 implements even older version of spec.\n",
+ " width = entry.contentRect.width;\n",
+ " height = entry.contentRect.height;\n",
+ " }\n",
+ "\n",
+ " // Keep the size of the canvas and rubber band canvas in sync with\n",
+ " // the canvas container.\n",
+ " if (entry.devicePixelContentBoxSize) {\n",
+ " // Chrome 84 implements new version of spec.\n",
+ " canvas.setAttribute(\n",
+ " 'width',\n",
+ " entry.devicePixelContentBoxSize[0].inlineSize\n",
+ " );\n",
+ " canvas.setAttribute(\n",
+ " 'height',\n",
+ " entry.devicePixelContentBoxSize[0].blockSize\n",
+ " );\n",
+ " } else {\n",
+ " canvas.setAttribute('width', width * fig.ratio);\n",
+ " canvas.setAttribute('height', height * fig.ratio);\n",
+ " }\n",
+ " /* This rescales the canvas back to display pixels, so that it\n",
+ " * appears correct on HiDPI screens. */\n",
+ " canvas.style.width = width + 'px';\n",
+ " canvas.style.height = height + 'px';\n",
+ "\n",
+ " rubberband_canvas.setAttribute('width', width);\n",
+ " rubberband_canvas.setAttribute('height', height);\n",
+ "\n",
+ " // And update the size in Python. We ignore the initial 0/0 size\n",
+ " // that occurs as the element is placed into the DOM, which should\n",
+ " // otherwise not happen due to the minimum size styling.\n",
+ " if (fig.ws.readyState == 1 && width != 0 && height != 0) {\n",
+ " fig.request_resize(width, height);\n",
+ " }\n",
+ " }\n",
+ " });\n",
+ " this.resizeObserverInstance.observe(canvas_div);\n",
+ "\n",
+ " function on_mouse_event_closure(name) {\n",
+ " /* User Agent sniffing is bad, but WebKit is busted:\n",
+ " * https://bugs.webkit.org/show_bug.cgi?id=144526\n",
+ " * https://bugs.webkit.org/show_bug.cgi?id=181818\n",
+ " * The worst that happens here is that they get an extra browser\n",
+ " * selection when dragging, if this check fails to catch them.\n",
+ " */\n",
+ " var UA = navigator.userAgent;\n",
+ " var isWebKit = /AppleWebKit/.test(UA) && !/Chrome/.test(UA);\n",
+ " if(isWebKit) {\n",
+ " return function (event) {\n",
+ " /* This prevents the web browser from automatically changing to\n",
+ " * the text insertion cursor when the button is pressed. We\n",
+ " * want to control all of the cursor setting manually through\n",
+ " * the 'cursor' event from matplotlib */\n",
+ " event.preventDefault()\n",
+ " return fig.mouse_event(event, name);\n",
+ " };\n",
+ " } else {\n",
+ " return function (event) {\n",
+ " return fig.mouse_event(event, name);\n",
+ " };\n",
+ " }\n",
+ " }\n",
+ "\n",
+ " canvas_div.addEventListener(\n",
+ " 'mousedown',\n",
+ " on_mouse_event_closure('button_press')\n",
+ " );\n",
+ " canvas_div.addEventListener(\n",
+ " 'mouseup',\n",
+ " on_mouse_event_closure('button_release')\n",
+ " );\n",
+ " canvas_div.addEventListener(\n",
+ " 'dblclick',\n",
+ " on_mouse_event_closure('dblclick')\n",
+ " );\n",
+ " // Throttle sequential mouse events to 1 every 20ms.\n",
+ " canvas_div.addEventListener(\n",
+ " 'mousemove',\n",
+ " on_mouse_event_closure('motion_notify')\n",
+ " );\n",
+ "\n",
+ " canvas_div.addEventListener(\n",
+ " 'mouseenter',\n",
+ " on_mouse_event_closure('figure_enter')\n",
+ " );\n",
+ " canvas_div.addEventListener(\n",
+ " 'mouseleave',\n",
+ " on_mouse_event_closure('figure_leave')\n",
+ " );\n",
+ "\n",
+ " canvas_div.addEventListener('wheel', function (event) {\n",
+ " if (event.deltaY < 0) {\n",
+ " event.step = 1;\n",
+ " } else {\n",
+ " event.step = -1;\n",
+ " }\n",
+ " on_mouse_event_closure('scroll')(event);\n",
+ " });\n",
+ "\n",
+ " canvas_div.appendChild(canvas);\n",
+ " canvas_div.appendChild(rubberband_canvas);\n",
+ "\n",
+ " this.rubberband_context = rubberband_canvas.getContext('2d');\n",
+ " this.rubberband_context.strokeStyle = '#000000';\n",
+ "\n",
+ " this._resize_canvas = function (width, height, forward) {\n",
+ " if (forward) {\n",
+ " canvas_div.style.width = width + 'px';\n",
+ " canvas_div.style.height = height + 'px';\n",
+ " }\n",
+ " };\n",
+ "\n",
+ " // Disable right mouse context menu.\n",
+ " canvas_div.addEventListener('contextmenu', function (_e) {\n",
+ " event.preventDefault();\n",
+ " return false;\n",
+ " });\n",
+ "\n",
+ " function set_focus() {\n",
+ " canvas.focus();\n",
+ " canvas_div.focus();\n",
+ " }\n",
+ "\n",
+ " window.setTimeout(set_focus, 100);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._init_toolbar = function () {\n",
+ " var fig = this;\n",
+ "\n",
+ " var toolbar = document.createElement('div');\n",
+ " toolbar.classList = 'mpl-toolbar';\n",
+ " this.root.appendChild(toolbar);\n",
+ "\n",
+ " function on_click_closure(name) {\n",
+ " return function (_event) {\n",
+ " return fig.toolbar_button_onclick(name);\n",
+ " };\n",
+ " }\n",
+ "\n",
+ " function on_mouseover_closure(tooltip) {\n",
+ " return function (event) {\n",
+ " if (!event.currentTarget.disabled) {\n",
+ " return fig.toolbar_button_onmouseover(tooltip);\n",
+ " }\n",
+ " };\n",
+ " }\n",
+ "\n",
+ " fig.buttons = {};\n",
+ " var buttonGroup = document.createElement('div');\n",
+ " buttonGroup.classList = 'mpl-button-group';\n",
+ " for (var toolbar_ind in mpl.toolbar_items) {\n",
+ " var name = mpl.toolbar_items[toolbar_ind][0];\n",
+ " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
+ " var image = mpl.toolbar_items[toolbar_ind][2];\n",
+ " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
+ "\n",
+ " if (!name) {\n",
+ " /* Instead of a spacer, we start a new button group. */\n",
+ " if (buttonGroup.hasChildNodes()) {\n",
+ " toolbar.appendChild(buttonGroup);\n",
+ " }\n",
+ " buttonGroup = document.createElement('div');\n",
+ " buttonGroup.classList = 'mpl-button-group';\n",
+ " continue;\n",
+ " }\n",
+ "\n",
+ " var button = (fig.buttons[name] = document.createElement('button'));\n",
+ " button.classList = 'mpl-widget';\n",
+ " button.setAttribute('role', 'button');\n",
+ " button.setAttribute('aria-disabled', 'false');\n",
+ " button.addEventListener('click', on_click_closure(method_name));\n",
+ " button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n",
+ "\n",
+ " var icon_img = document.createElement('img');\n",
+ " icon_img.src = '_images/' + image + '.png';\n",
+ " icon_img.srcset = '_images/' + image + '_large.png 2x';\n",
+ " icon_img.alt = tooltip;\n",
+ " button.appendChild(icon_img);\n",
+ "\n",
+ " buttonGroup.appendChild(button);\n",
+ " }\n",
+ "\n",
+ " if (buttonGroup.hasChildNodes()) {\n",
+ " toolbar.appendChild(buttonGroup);\n",
+ " }\n",
+ "\n",
+ " var fmt_picker = document.createElement('select');\n",
+ " fmt_picker.classList = 'mpl-widget';\n",
+ " toolbar.appendChild(fmt_picker);\n",
+ " this.format_dropdown = fmt_picker;\n",
+ "\n",
+ " for (var ind in mpl.extensions) {\n",
+ " var fmt = mpl.extensions[ind];\n",
+ " var option = document.createElement('option');\n",
+ " option.selected = fmt === mpl.default_extension;\n",
+ " option.innerHTML = fmt;\n",
+ " fmt_picker.appendChild(option);\n",
+ " }\n",
+ "\n",
+ " var status_bar = document.createElement('span');\n",
+ " status_bar.classList = 'mpl-message';\n",
+ " toolbar.appendChild(status_bar);\n",
+ " this.message = status_bar;\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.request_resize = function (x_pixels, y_pixels) {\n",
+ " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n",
+ " // which will in turn request a refresh of the image.\n",
+ " this.send_message('resize', { width: x_pixels, height: y_pixels });\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.send_message = function (type, properties) {\n",
+ " properties['type'] = type;\n",
+ " properties['figure_id'] = this.id;\n",
+ " this.ws.send(JSON.stringify(properties));\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.send_draw_message = function () {\n",
+ " if (!this.waiting) {\n",
+ " this.waiting = true;\n",
+ " this.ws.send(JSON.stringify({ type: 'draw', figure_id: this.id }));\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_save = function (fig, _msg) {\n",
+ " var format_dropdown = fig.format_dropdown;\n",
+ " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n",
+ " fig.ondownload(fig, format);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_resize = function (fig, msg) {\n",
+ " var size = msg['size'];\n",
+ " if (size[0] !== fig.canvas.width || size[1] !== fig.canvas.height) {\n",
+ " fig._resize_canvas(size[0], size[1], msg['forward']);\n",
+ " fig.send_message('refresh', {});\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_rubberband = function (fig, msg) {\n",
+ " var x0 = msg['x0'] / fig.ratio;\n",
+ " var y0 = (fig.canvas.height - msg['y0']) / fig.ratio;\n",
+ " var x1 = msg['x1'] / fig.ratio;\n",
+ " var y1 = (fig.canvas.height - msg['y1']) / fig.ratio;\n",
+ " x0 = Math.floor(x0) + 0.5;\n",
+ " y0 = Math.floor(y0) + 0.5;\n",
+ " x1 = Math.floor(x1) + 0.5;\n",
+ " y1 = Math.floor(y1) + 0.5;\n",
+ " var min_x = Math.min(x0, x1);\n",
+ " var min_y = Math.min(y0, y1);\n",
+ " var width = Math.abs(x1 - x0);\n",
+ " var height = Math.abs(y1 - y0);\n",
+ "\n",
+ " fig.rubberband_context.clearRect(\n",
+ " 0,\n",
+ " 0,\n",
+ " fig.canvas.width / fig.ratio,\n",
+ " fig.canvas.height / fig.ratio\n",
+ " );\n",
+ "\n",
+ " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_figure_label = function (fig, msg) {\n",
+ " // Updates the figure title.\n",
+ " fig.header.textContent = msg['label'];\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_cursor = function (fig, msg) {\n",
+ " fig.canvas_div.style.cursor = msg['cursor'];\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_message = function (fig, msg) {\n",
+ " fig.message.textContent = msg['message'];\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_draw = function (fig, _msg) {\n",
+ " // Request the server to send over a new figure.\n",
+ " fig.send_draw_message();\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_image_mode = function (fig, msg) {\n",
+ " fig.image_mode = msg['mode'];\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_history_buttons = function (fig, msg) {\n",
+ " for (var key in msg) {\n",
+ " if (!(key in fig.buttons)) {\n",
+ " continue;\n",
+ " }\n",
+ " fig.buttons[key].disabled = !msg[key];\n",
+ " fig.buttons[key].setAttribute('aria-disabled', !msg[key]);\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_navigate_mode = function (fig, msg) {\n",
+ " if (msg['mode'] === 'PAN') {\n",
+ " fig.buttons['Pan'].classList.add('active');\n",
+ " fig.buttons['Zoom'].classList.remove('active');\n",
+ " } else if (msg['mode'] === 'ZOOM') {\n",
+ " fig.buttons['Pan'].classList.remove('active');\n",
+ " fig.buttons['Zoom'].classList.add('active');\n",
+ " } else {\n",
+ " fig.buttons['Pan'].classList.remove('active');\n",
+ " fig.buttons['Zoom'].classList.remove('active');\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.updated_canvas_event = function () {\n",
+ " // Called whenever the canvas gets updated.\n",
+ " this.send_message('ack', {});\n",
+ "};\n",
+ "\n",
+ "// A function to construct a web socket function for onmessage handling.\n",
+ "// Called in the figure constructor.\n",
+ "mpl.figure.prototype._make_on_message_function = function (fig) {\n",
+ " return function socket_on_message(evt) {\n",
+ " if (evt.data instanceof Blob) {\n",
+ " var img = evt.data;\n",
+ " if (img.type !== 'image/png') {\n",
+ " /* FIXME: We get \"Resource interpreted as Image but\n",
+ " * transferred with MIME type text/plain:\" errors on\n",
+ " * Chrome. But how to set the MIME type? It doesn't seem\n",
+ " * to be part of the websocket stream */\n",
+ " img.type = 'image/png';\n",
+ " }\n",
+ "\n",
+ " /* Free the memory for the previous frames */\n",
+ " if (fig.imageObj.src) {\n",
+ " (window.URL || window.webkitURL).revokeObjectURL(\n",
+ " fig.imageObj.src\n",
+ " );\n",
+ " }\n",
+ "\n",
+ " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
+ " img\n",
+ " );\n",
+ " fig.updated_canvas_event();\n",
+ " fig.waiting = false;\n",
+ " return;\n",
+ " } else if (\n",
+ " typeof evt.data === 'string' &&\n",
+ " evt.data.slice(0, 21) === 'data:image/png;base64'\n",
+ " ) {\n",
+ " fig.imageObj.src = evt.data;\n",
+ " fig.updated_canvas_event();\n",
+ " fig.waiting = false;\n",
+ " return;\n",
+ " }\n",
+ "\n",
+ " var msg = JSON.parse(evt.data);\n",
+ " var msg_type = msg['type'];\n",
+ "\n",
+ " // Call the \"handle_{type}\" callback, which takes\n",
+ " // the figure and JSON message as its only arguments.\n",
+ " try {\n",
+ " var callback = fig['handle_' + msg_type];\n",
+ " } catch (e) {\n",
+ " console.log(\n",
+ " \"No handler for the '\" + msg_type + \"' message type: \",\n",
+ " msg\n",
+ " );\n",
+ " return;\n",
+ " }\n",
+ "\n",
+ " if (callback) {\n",
+ " try {\n",
+ " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n",
+ " callback(fig, msg);\n",
+ " } catch (e) {\n",
+ " console.log(\n",
+ " \"Exception inside the 'handler_\" + msg_type + \"' callback:\",\n",
+ " e,\n",
+ " e.stack,\n",
+ " msg\n",
+ " );\n",
+ " }\n",
+ " }\n",
+ " };\n",
+ "};\n",
+ "\n",
+ "function getModifiers(event) {\n",
+ " var mods = [];\n",
+ " if (event.ctrlKey) {\n",
+ " mods.push('ctrl');\n",
+ " }\n",
+ " if (event.altKey) {\n",
+ " mods.push('alt');\n",
+ " }\n",
+ " if (event.shiftKey) {\n",
+ " mods.push('shift');\n",
+ " }\n",
+ " if (event.metaKey) {\n",
+ " mods.push('meta');\n",
+ " }\n",
+ " return mods;\n",
+ "}\n",
+ "\n",
+ "/*\n",
+ " * return a copy of an object with only non-object keys\n",
+ " * we need this to avoid circular references\n",
+ " * https://stackoverflow.com/a/24161582/3208463\n",
+ " */\n",
+ "function simpleKeys(original) {\n",
+ " return Object.keys(original).reduce(function (obj, key) {\n",
+ " if (typeof original[key] !== 'object') {\n",
+ " obj[key] = original[key];\n",
+ " }\n",
+ " return obj;\n",
+ " }, {});\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.mouse_event = function (event, name) {\n",
+ " if (name === 'button_press') {\n",
+ " this.canvas.focus();\n",
+ " this.canvas_div.focus();\n",
+ " }\n",
+ "\n",
+ " // from https://stackoverflow.com/q/1114465\n",
+ " var boundingRect = this.canvas.getBoundingClientRect();\n",
+ " var x = (event.clientX - boundingRect.left) * this.ratio;\n",
+ " var y = (event.clientY - boundingRect.top) * this.ratio;\n",
+ "\n",
+ " this.send_message(name, {\n",
+ " x: x,\n",
+ " y: y,\n",
+ " button: event.button,\n",
+ " step: event.step,\n",
+ " modifiers: getModifiers(event),\n",
+ " guiEvent: simpleKeys(event),\n",
+ " });\n",
+ "\n",
+ " return false;\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._key_event_extra = function (_event, _name) {\n",
+ " // Handle any extra behaviour associated with a key event\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.key_event = function (event, name) {\n",
+ " // Prevent repeat events\n",
+ " if (name === 'key_press') {\n",
+ " if (event.key === this._key) {\n",
+ " return;\n",
+ " } else {\n",
+ " this._key = event.key;\n",
+ " }\n",
+ " }\n",
+ " if (name === 'key_release') {\n",
+ " this._key = null;\n",
+ " }\n",
+ "\n",
+ " var value = '';\n",
+ " if (event.ctrlKey && event.key !== 'Control') {\n",
+ " value += 'ctrl+';\n",
+ " }\n",
+ " else if (event.altKey && event.key !== 'Alt') {\n",
+ " value += 'alt+';\n",
+ " }\n",
+ " else if (event.shiftKey && event.key !== 'Shift') {\n",
+ " value += 'shift+';\n",
+ " }\n",
+ "\n",
+ " value += 'k' + event.key;\n",
+ "\n",
+ " this._key_event_extra(event, name);\n",
+ "\n",
+ " this.send_message(name, { key: value, guiEvent: simpleKeys(event) });\n",
+ " return false;\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.toolbar_button_onclick = function (name) {\n",
+ " if (name === 'download') {\n",
+ " this.handle_save(this, null);\n",
+ " } else {\n",
+ " this.send_message('toolbar_button', { name: name });\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.toolbar_button_onmouseover = function (tooltip) {\n",
+ " this.message.textContent = tooltip;\n",
+ "};\n",
+ "\n",
+ "///////////////// REMAINING CONTENT GENERATED BY embed_js.py /////////////////\n",
+ "// prettier-ignore\n",
+ "var _JSXTOOLS_RESIZE_OBSERVER=function(A){var t,i=new WeakMap,n=new WeakMap,a=new WeakMap,r=new WeakMap,o=new Set;function s(e){if(!(this instanceof s))throw new TypeError(\"Constructor requires 'new' operator\");i.set(this,e)}function h(){throw new TypeError(\"Function is not a constructor\")}function c(e,t,i,n){e=0 in arguments?Number(arguments[0]):0,t=1 in arguments?Number(arguments[1]):0,i=2 in arguments?Number(arguments[2]):0,n=3 in arguments?Number(arguments[3]):0,this.right=(this.x=this.left=e)+(this.width=i),this.bottom=(this.y=this.top=t)+(this.height=n),Object.freeze(this)}function d(){t=requestAnimationFrame(d);var s=new WeakMap,p=new Set;o.forEach((function(t){r.get(t).forEach((function(i){var r=t instanceof window.SVGElement,o=a.get(t),d=r?0:parseFloat(o.paddingTop),f=r?0:parseFloat(o.paddingRight),l=r?0:parseFloat(o.paddingBottom),u=r?0:parseFloat(o.paddingLeft),g=r?0:parseFloat(o.borderTopWidth),m=r?0:parseFloat(o.borderRightWidth),w=r?0:parseFloat(o.borderBottomWidth),b=u+f,F=d+l,v=(r?0:parseFloat(o.borderLeftWidth))+m,W=g+w,y=r?0:t.offsetHeight-W-t.clientHeight,E=r?0:t.offsetWidth-v-t.clientWidth,R=b+v,z=F+W,M=r?t.width:parseFloat(o.width)-R-E,O=r?t.height:parseFloat(o.height)-z-y;if(n.has(t)){var k=n.get(t);if(k[0]===M&&k[1]===O)return}n.set(t,[M,O]);var S=Object.create(h.prototype);S.target=t,S.contentRect=new c(u,d,M,O),s.has(i)||(s.set(i,[]),p.add(i)),s.get(i).push(S)}))})),p.forEach((function(e){i.get(e).call(e,s.get(e),e)}))}return s.prototype.observe=function(i){if(i instanceof window.Element){r.has(i)||(r.set(i,new Set),o.add(i),a.set(i,window.getComputedStyle(i)));var n=r.get(i);n.has(this)||n.add(this),cancelAnimationFrame(t),t=requestAnimationFrame(d)}},s.prototype.unobserve=function(i){if(i instanceof window.Element&&r.has(i)){var n=r.get(i);n.has(this)&&(n.delete(this),n.size||(r.delete(i),o.delete(i))),n.size||r.delete(i),o.size||cancelAnimationFrame(t)}},A.DOMRectReadOnly=c,A.ResizeObserver=s,A.ResizeObserverEntry=h,A}; // eslint-disable-line\n",
+ "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Left button pans, Right button zooms\\nx/y fixes axis, CTRL fixes aspect\", \"fa fa-arrows\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\\nx/y fixes axis\", \"fa fa-square-o\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o\", \"download\"]];\n",
+ "\n",
+ "mpl.extensions = [\"eps\", \"jpeg\", \"pgf\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\", \"tif\", \"webp\"];\n",
+ "\n",
+ "mpl.default_extension = \"png\";/* global mpl */\n",
+ "\n",
+ "var comm_websocket_adapter = function (comm) {\n",
+ " // Create a \"websocket\"-like object which calls the given IPython comm\n",
+ " // object with the appropriate methods. Currently this is a non binary\n",
+ " // socket, so there is still some room for performance tuning.\n",
+ " var ws = {};\n",
+ "\n",
+ " ws.binaryType = comm.kernel.ws.binaryType;\n",
+ " ws.readyState = comm.kernel.ws.readyState;\n",
+ " function updateReadyState(_event) {\n",
+ " if (comm.kernel.ws) {\n",
+ " ws.readyState = comm.kernel.ws.readyState;\n",
+ " } else {\n",
+ " ws.readyState = 3; // Closed state.\n",
+ " }\n",
+ " }\n",
+ " comm.kernel.ws.addEventListener('open', updateReadyState);\n",
+ " comm.kernel.ws.addEventListener('close', updateReadyState);\n",
+ " comm.kernel.ws.addEventListener('error', updateReadyState);\n",
+ "\n",
+ " ws.close = function () {\n",
+ " comm.close();\n",
+ " };\n",
+ " ws.send = function (m) {\n",
+ " //console.log('sending', m);\n",
+ " comm.send(m);\n",
+ " };\n",
+ " // Register the callback with on_msg.\n",
+ " comm.on_msg(function (msg) {\n",
+ " //console.log('receiving', msg['content']['data'], msg);\n",
+ " var data = msg['content']['data'];\n",
+ " if (data['blob'] !== undefined) {\n",
+ " data = {\n",
+ " data: new Blob(msg['buffers'], { type: data['blob'] }),\n",
+ " };\n",
+ " }\n",
+ " // Pass the mpl event to the overridden (by mpl) onmessage function.\n",
+ " ws.onmessage(data);\n",
+ " });\n",
+ " return ws;\n",
+ "};\n",
+ "\n",
+ "mpl.mpl_figure_comm = function (comm, msg) {\n",
+ " // This is the function which gets called when the mpl process\n",
+ " // starts-up an IPython Comm through the \"matplotlib\" channel.\n",
+ "\n",
+ " var id = msg.content.data.id;\n",
+ " // Get hold of the div created by the display call when the Comm\n",
+ " // socket was opened in Python.\n",
+ " var element = document.getElementById(id);\n",
+ " var ws_proxy = comm_websocket_adapter(comm);\n",
+ "\n",
+ " function ondownload(figure, _format) {\n",
+ " window.open(figure.canvas.toDataURL());\n",
+ " }\n",
+ "\n",
+ " var fig = new mpl.figure(id, ws_proxy, ondownload, element);\n",
+ "\n",
+ " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n",
+ " // web socket which is closed, not our websocket->open comm proxy.\n",
+ " ws_proxy.onopen();\n",
+ "\n",
+ " fig.parent_element = element;\n",
+ " fig.cell_info = mpl.find_output_cell(\"\");\n",
+ " if (!fig.cell_info) {\n",
+ " console.error('Failed to find cell for figure', id, fig);\n",
+ " return;\n",
+ " }\n",
+ " fig.cell_info[0].output_area.element.on(\n",
+ " 'cleared',\n",
+ " { fig: fig },\n",
+ " fig._remove_fig_handler\n",
+ " );\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_close = function (fig, msg) {\n",
+ " var width = fig.canvas.width / fig.ratio;\n",
+ " fig.cell_info[0].output_area.element.off(\n",
+ " 'cleared',\n",
+ " fig._remove_fig_handler\n",
+ " );\n",
+ " fig.resizeObserverInstance.unobserve(fig.canvas_div);\n",
+ "\n",
+ " // Update the output cell to use the data from the current canvas.\n",
+ " fig.push_to_output();\n",
+ " var dataURL = fig.canvas.toDataURL();\n",
+ " // Re-enable the keyboard manager in IPython - without this line, in FF,\n",
+ " // the notebook keyboard shortcuts fail.\n",
+ " IPython.keyboard_manager.enable();\n",
+ " fig.parent_element.innerHTML =\n",
+ " '';\n",
+ " fig.close_ws(fig, msg);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.close_ws = function (fig, msg) {\n",
+ " fig.send_message('closing', msg);\n",
+ " // fig.ws.close()\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.push_to_output = function (_remove_interactive) {\n",
+ " // Turn the data on the canvas into data in the output cell.\n",
+ " var width = this.canvas.width / this.ratio;\n",
+ " var dataURL = this.canvas.toDataURL();\n",
+ " this.cell_info[1]['text/html'] =\n",
+ " '';\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.updated_canvas_event = function () {\n",
+ " // Tell IPython that the notebook contents must change.\n",
+ " IPython.notebook.set_dirty(true);\n",
+ " this.send_message('ack', {});\n",
+ " var fig = this;\n",
+ " // Wait a second, then push the new image to the DOM so\n",
+ " // that it is saved nicely (might be nice to debounce this).\n",
+ " setTimeout(function () {\n",
+ " fig.push_to_output();\n",
+ " }, 1000);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._init_toolbar = function () {\n",
+ " var fig = this;\n",
+ "\n",
+ " var toolbar = document.createElement('div');\n",
+ " toolbar.classList = 'btn-toolbar';\n",
+ " this.root.appendChild(toolbar);\n",
+ "\n",
+ " function on_click_closure(name) {\n",
+ " return function (_event) {\n",
+ " return fig.toolbar_button_onclick(name);\n",
+ " };\n",
+ " }\n",
+ "\n",
+ " function on_mouseover_closure(tooltip) {\n",
+ " return function (event) {\n",
+ " if (!event.currentTarget.disabled) {\n",
+ " return fig.toolbar_button_onmouseover(tooltip);\n",
+ " }\n",
+ " };\n",
+ " }\n",
+ "\n",
+ " fig.buttons = {};\n",
+ " var buttonGroup = document.createElement('div');\n",
+ " buttonGroup.classList = 'btn-group';\n",
+ " var button;\n",
+ " for (var toolbar_ind in mpl.toolbar_items) {\n",
+ " var name = mpl.toolbar_items[toolbar_ind][0];\n",
+ " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
+ " var image = mpl.toolbar_items[toolbar_ind][2];\n",
+ " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
+ "\n",
+ " if (!name) {\n",
+ " /* Instead of a spacer, we start a new button group. */\n",
+ " if (buttonGroup.hasChildNodes()) {\n",
+ " toolbar.appendChild(buttonGroup);\n",
+ " }\n",
+ " buttonGroup = document.createElement('div');\n",
+ " buttonGroup.classList = 'btn-group';\n",
+ " continue;\n",
+ " }\n",
+ "\n",
+ " button = fig.buttons[name] = document.createElement('button');\n",
+ " button.classList = 'btn btn-default';\n",
+ " button.href = '#';\n",
+ " button.title = name;\n",
+ " button.innerHTML = '';\n",
+ " button.addEventListener('click', on_click_closure(method_name));\n",
+ " button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n",
+ " buttonGroup.appendChild(button);\n",
+ " }\n",
+ "\n",
+ " if (buttonGroup.hasChildNodes()) {\n",
+ " toolbar.appendChild(buttonGroup);\n",
+ " }\n",
+ "\n",
+ " // Add the status bar.\n",
+ " var status_bar = document.createElement('span');\n",
+ " status_bar.classList = 'mpl-message pull-right';\n",
+ " toolbar.appendChild(status_bar);\n",
+ " this.message = status_bar;\n",
+ "\n",
+ " // Add the close button to the window.\n",
+ " var buttongrp = document.createElement('div');\n",
+ " buttongrp.classList = 'btn-group inline pull-right';\n",
+ " button = document.createElement('button');\n",
+ " button.classList = 'btn btn-mini btn-primary';\n",
+ " button.href = '#';\n",
+ " button.title = 'Stop Interaction';\n",
+ " button.innerHTML = '';\n",
+ " button.addEventListener('click', function (_evt) {\n",
+ " fig.handle_close(fig, {});\n",
+ " });\n",
+ " button.addEventListener(\n",
+ " 'mouseover',\n",
+ " on_mouseover_closure('Stop Interaction')\n",
+ " );\n",
+ " buttongrp.appendChild(button);\n",
+ " var titlebar = this.root.querySelector('.ui-dialog-titlebar');\n",
+ " titlebar.insertBefore(buttongrp, titlebar.firstChild);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._remove_fig_handler = function (event) {\n",
+ " var fig = event.data.fig;\n",
+ " if (event.target !== this) {\n",
+ " // Ignore bubbled events from children.\n",
+ " return;\n",
+ " }\n",
+ " fig.close_ws(fig, {});\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._root_extra_style = function (el) {\n",
+ " el.style.boxSizing = 'content-box'; // override notebook setting of border-box.\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._canvas_extra_style = function (el) {\n",
+ " // this is important to make the div 'focusable\n",
+ " el.setAttribute('tabindex', 0);\n",
+ " // reach out to IPython and tell the keyboard manager to turn it's self\n",
+ " // off when our div gets focus\n",
+ "\n",
+ " // location in version 3\n",
+ " if (IPython.notebook.keyboard_manager) {\n",
+ " IPython.notebook.keyboard_manager.register_events(el);\n",
+ " } else {\n",
+ " // location in version 2\n",
+ " IPython.keyboard_manager.register_events(el);\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._key_event_extra = function (event, _name) {\n",
+ " // Check for shift+enter\n",
+ " if (event.shiftKey && event.which === 13) {\n",
+ " this.canvas_div.blur();\n",
+ " // select the cell after this one\n",
+ " var index = IPython.notebook.find_cell_index(this.cell_info[0]);\n",
+ " IPython.notebook.select(index + 1);\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_save = function (fig, _msg) {\n",
+ " fig.ondownload(fig, null);\n",
+ "};\n",
+ "\n",
+ "mpl.find_output_cell = function (html_output) {\n",
+ " // Return the cell and output element which can be found *uniquely* in the notebook.\n",
+ " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n",
+ " // IPython event is triggered only after the cells have been serialised, which for\n",
+ " // our purposes (turning an active figure into a static one), is too late.\n",
+ " var cells = IPython.notebook.get_cells();\n",
+ " var ncells = cells.length;\n",
+ " for (var i = 0; i < ncells; i++) {\n",
+ " var cell = cells[i];\n",
+ " if (cell.cell_type === 'code') {\n",
+ " for (var j = 0; j < cell.output_area.outputs.length; j++) {\n",
+ " var data = cell.output_area.outputs[j];\n",
+ " if (data.data) {\n",
+ " // IPython >= 3 moved mimebundle to data attribute of output\n",
+ " data = data.data;\n",
+ " }\n",
+ " if (data['text/html'] === html_output) {\n",
+ " return [cell, data, j];\n",
+ " }\n",
+ " }\n",
+ " }\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "// Register the function which deals with the matplotlib target/channel.\n",
+ "// The kernel may be null if the page has been refreshed.\n",
+ "if (IPython.notebook.kernel !== null) {\n",
+ " IPython.notebook.kernel.comm_manager.register_target(\n",
+ " 'matplotlib',\n",
+ " mpl.mpl_figure_comm\n",
+ " );\n",
+ "}\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/html": [
+ ""
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/javascript": [
+ "/* Put everything inside the global mpl namespace */\n",
+ "/* global mpl */\n",
+ "window.mpl = {};\n",
+ "\n",
+ "mpl.get_websocket_type = function () {\n",
+ " if (typeof WebSocket !== 'undefined') {\n",
+ " return WebSocket;\n",
+ " } else if (typeof MozWebSocket !== 'undefined') {\n",
+ " return MozWebSocket;\n",
+ " } else {\n",
+ " alert(\n",
+ " 'Your browser does not have WebSocket support. ' +\n",
+ " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n",
+ " 'Firefox 4 and 5 are also supported but you ' +\n",
+ " 'have to enable WebSockets in about:config.'\n",
+ " );\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure = function (figure_id, websocket, ondownload, parent_element) {\n",
+ " this.id = figure_id;\n",
+ "\n",
+ " this.ws = websocket;\n",
+ "\n",
+ " this.supports_binary = this.ws.binaryType !== undefined;\n",
+ "\n",
+ " if (!this.supports_binary) {\n",
+ " var warnings = document.getElementById('mpl-warnings');\n",
+ " if (warnings) {\n",
+ " warnings.style.display = 'block';\n",
+ " warnings.textContent =\n",
+ " 'This browser does not support binary websocket messages. ' +\n",
+ " 'Performance may be slow.';\n",
+ " }\n",
+ " }\n",
+ "\n",
+ " this.imageObj = new Image();\n",
+ "\n",
+ " this.context = undefined;\n",
+ " this.message = undefined;\n",
+ " this.canvas = undefined;\n",
+ " this.rubberband_canvas = undefined;\n",
+ " this.rubberband_context = undefined;\n",
+ " this.format_dropdown = undefined;\n",
+ "\n",
+ " this.image_mode = 'full';\n",
+ "\n",
+ " this.root = document.createElement('div');\n",
+ " this.root.setAttribute('style', 'display: inline-block');\n",
+ " this._root_extra_style(this.root);\n",
+ "\n",
+ " parent_element.appendChild(this.root);\n",
+ "\n",
+ " this._init_header(this);\n",
+ " this._init_canvas(this);\n",
+ " this._init_toolbar(this);\n",
+ "\n",
+ " var fig = this;\n",
+ "\n",
+ " this.waiting = false;\n",
+ "\n",
+ " this.ws.onopen = function () {\n",
+ " fig.send_message('supports_binary', { value: fig.supports_binary });\n",
+ " fig.send_message('send_image_mode', {});\n",
+ " if (fig.ratio !== 1) {\n",
+ " fig.send_message('set_device_pixel_ratio', {\n",
+ " device_pixel_ratio: fig.ratio,\n",
+ " });\n",
+ " }\n",
+ " fig.send_message('refresh', {});\n",
+ " };\n",
+ "\n",
+ " this.imageObj.onload = function () {\n",
+ " if (fig.image_mode === 'full') {\n",
+ " // Full images could contain transparency (where diff images\n",
+ " // almost always do), so we need to clear the canvas so that\n",
+ " // there is no ghosting.\n",
+ " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n",
+ " }\n",
+ " fig.context.drawImage(fig.imageObj, 0, 0);\n",
+ " };\n",
+ "\n",
+ " this.imageObj.onunload = function () {\n",
+ " fig.ws.close();\n",
+ " };\n",
+ "\n",
+ " this.ws.onmessage = this._make_on_message_function(this);\n",
+ "\n",
+ " this.ondownload = ondownload;\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._init_header = function () {\n",
+ " var titlebar = document.createElement('div');\n",
+ " titlebar.classList =\n",
+ " 'ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix';\n",
+ " var titletext = document.createElement('div');\n",
+ " titletext.classList = 'ui-dialog-title';\n",
+ " titletext.setAttribute(\n",
+ " 'style',\n",
+ " 'width: 100%; text-align: center; padding: 3px;'\n",
+ " );\n",
+ " titlebar.appendChild(titletext);\n",
+ " this.root.appendChild(titlebar);\n",
+ " this.header = titletext;\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._canvas_extra_style = function (_canvas_div) {};\n",
+ "\n",
+ "mpl.figure.prototype._root_extra_style = function (_canvas_div) {};\n",
+ "\n",
+ "mpl.figure.prototype._init_canvas = function () {\n",
+ " var fig = this;\n",
+ "\n",
+ " var canvas_div = (this.canvas_div = document.createElement('div'));\n",
+ " canvas_div.setAttribute('tabindex', '0');\n",
+ " canvas_div.setAttribute(\n",
+ " 'style',\n",
+ " 'border: 1px solid #ddd;' +\n",
+ " 'box-sizing: content-box;' +\n",
+ " 'clear: both;' +\n",
+ " 'min-height: 1px;' +\n",
+ " 'min-width: 1px;' +\n",
+ " 'outline: 0;' +\n",
+ " 'overflow: hidden;' +\n",
+ " 'position: relative;' +\n",
+ " 'resize: both;' +\n",
+ " 'z-index: 2;'\n",
+ " );\n",
+ "\n",
+ " function on_keyboard_event_closure(name) {\n",
+ " return function (event) {\n",
+ " return fig.key_event(event, name);\n",
+ " };\n",
+ " }\n",
+ "\n",
+ " canvas_div.addEventListener(\n",
+ " 'keydown',\n",
+ " on_keyboard_event_closure('key_press')\n",
+ " );\n",
+ " canvas_div.addEventListener(\n",
+ " 'keyup',\n",
+ " on_keyboard_event_closure('key_release')\n",
+ " );\n",
+ "\n",
+ " this._canvas_extra_style(canvas_div);\n",
+ " this.root.appendChild(canvas_div);\n",
+ "\n",
+ " var canvas = (this.canvas = document.createElement('canvas'));\n",
+ " canvas.classList.add('mpl-canvas');\n",
+ " canvas.setAttribute(\n",
+ " 'style',\n",
+ " 'box-sizing: content-box;' +\n",
+ " 'pointer-events: none;' +\n",
+ " 'position: relative;' +\n",
+ " 'z-index: 0;'\n",
+ " );\n",
+ "\n",
+ " this.context = canvas.getContext('2d');\n",
+ "\n",
+ " var backingStore =\n",
+ " this.context.backingStorePixelRatio ||\n",
+ " this.context.webkitBackingStorePixelRatio ||\n",
+ " this.context.mozBackingStorePixelRatio ||\n",
+ " this.context.msBackingStorePixelRatio ||\n",
+ " this.context.oBackingStorePixelRatio ||\n",
+ " this.context.backingStorePixelRatio ||\n",
+ " 1;\n",
+ "\n",
+ " this.ratio = (window.devicePixelRatio || 1) / backingStore;\n",
+ "\n",
+ " var rubberband_canvas = (this.rubberband_canvas = document.createElement(\n",
+ " 'canvas'\n",
+ " ));\n",
+ " rubberband_canvas.setAttribute(\n",
+ " 'style',\n",
+ " 'box-sizing: content-box;' +\n",
+ " 'left: 0;' +\n",
+ " 'pointer-events: none;' +\n",
+ " 'position: absolute;' +\n",
+ " 'top: 0;' +\n",
+ " 'z-index: 1;'\n",
+ " );\n",
+ "\n",
+ " // Apply a ponyfill if ResizeObserver is not implemented by browser.\n",
+ " if (this.ResizeObserver === undefined) {\n",
+ " if (window.ResizeObserver !== undefined) {\n",
+ " this.ResizeObserver = window.ResizeObserver;\n",
+ " } else {\n",
+ " var obs = _JSXTOOLS_RESIZE_OBSERVER({});\n",
+ " this.ResizeObserver = obs.ResizeObserver;\n",
+ " }\n",
+ " }\n",
+ "\n",
+ " this.resizeObserverInstance = new this.ResizeObserver(function (entries) {\n",
+ " var nentries = entries.length;\n",
+ " for (var i = 0; i < nentries; i++) {\n",
+ " var entry = entries[i];\n",
+ " var width, height;\n",
+ " if (entry.contentBoxSize) {\n",
+ " if (entry.contentBoxSize instanceof Array) {\n",
+ " // Chrome 84 implements new version of spec.\n",
+ " width = entry.contentBoxSize[0].inlineSize;\n",
+ " height = entry.contentBoxSize[0].blockSize;\n",
+ " } else {\n",
+ " // Firefox implements old version of spec.\n",
+ " width = entry.contentBoxSize.inlineSize;\n",
+ " height = entry.contentBoxSize.blockSize;\n",
+ " }\n",
+ " } else {\n",
+ " // Chrome <84 implements even older version of spec.\n",
+ " width = entry.contentRect.width;\n",
+ " height = entry.contentRect.height;\n",
+ " }\n",
+ "\n",
+ " // Keep the size of the canvas and rubber band canvas in sync with\n",
+ " // the canvas container.\n",
+ " if (entry.devicePixelContentBoxSize) {\n",
+ " // Chrome 84 implements new version of spec.\n",
+ " canvas.setAttribute(\n",
+ " 'width',\n",
+ " entry.devicePixelContentBoxSize[0].inlineSize\n",
+ " );\n",
+ " canvas.setAttribute(\n",
+ " 'height',\n",
+ " entry.devicePixelContentBoxSize[0].blockSize\n",
+ " );\n",
+ " } else {\n",
+ " canvas.setAttribute('width', width * fig.ratio);\n",
+ " canvas.setAttribute('height', height * fig.ratio);\n",
+ " }\n",
+ " /* This rescales the canvas back to display pixels, so that it\n",
+ " * appears correct on HiDPI screens. */\n",
+ " canvas.style.width = width + 'px';\n",
+ " canvas.style.height = height + 'px';\n",
+ "\n",
+ " rubberband_canvas.setAttribute('width', width);\n",
+ " rubberband_canvas.setAttribute('height', height);\n",
+ "\n",
+ " // And update the size in Python. We ignore the initial 0/0 size\n",
+ " // that occurs as the element is placed into the DOM, which should\n",
+ " // otherwise not happen due to the minimum size styling.\n",
+ " if (fig.ws.readyState == 1 && width != 0 && height != 0) {\n",
+ " fig.request_resize(width, height);\n",
+ " }\n",
+ " }\n",
+ " });\n",
+ " this.resizeObserverInstance.observe(canvas_div);\n",
+ "\n",
+ " function on_mouse_event_closure(name) {\n",
+ " /* User Agent sniffing is bad, but WebKit is busted:\n",
+ " * https://bugs.webkit.org/show_bug.cgi?id=144526\n",
+ " * https://bugs.webkit.org/show_bug.cgi?id=181818\n",
+ " * The worst that happens here is that they get an extra browser\n",
+ " * selection when dragging, if this check fails to catch them.\n",
+ " */\n",
+ " var UA = navigator.userAgent;\n",
+ " var isWebKit = /AppleWebKit/.test(UA) && !/Chrome/.test(UA);\n",
+ " if(isWebKit) {\n",
+ " return function (event) {\n",
+ " /* This prevents the web browser from automatically changing to\n",
+ " * the text insertion cursor when the button is pressed. We\n",
+ " * want to control all of the cursor setting manually through\n",
+ " * the 'cursor' event from matplotlib */\n",
+ " event.preventDefault()\n",
+ " return fig.mouse_event(event, name);\n",
+ " };\n",
+ " } else {\n",
+ " return function (event) {\n",
+ " return fig.mouse_event(event, name);\n",
+ " };\n",
+ " }\n",
+ " }\n",
+ "\n",
+ " canvas_div.addEventListener(\n",
+ " 'mousedown',\n",
+ " on_mouse_event_closure('button_press')\n",
+ " );\n",
+ " canvas_div.addEventListener(\n",
+ " 'mouseup',\n",
+ " on_mouse_event_closure('button_release')\n",
+ " );\n",
+ " canvas_div.addEventListener(\n",
+ " 'dblclick',\n",
+ " on_mouse_event_closure('dblclick')\n",
+ " );\n",
+ " // Throttle sequential mouse events to 1 every 20ms.\n",
+ " canvas_div.addEventListener(\n",
+ " 'mousemove',\n",
+ " on_mouse_event_closure('motion_notify')\n",
+ " );\n",
+ "\n",
+ " canvas_div.addEventListener(\n",
+ " 'mouseenter',\n",
+ " on_mouse_event_closure('figure_enter')\n",
+ " );\n",
+ " canvas_div.addEventListener(\n",
+ " 'mouseleave',\n",
+ " on_mouse_event_closure('figure_leave')\n",
+ " );\n",
+ "\n",
+ " canvas_div.addEventListener('wheel', function (event) {\n",
+ " if (event.deltaY < 0) {\n",
+ " event.step = 1;\n",
+ " } else {\n",
+ " event.step = -1;\n",
+ " }\n",
+ " on_mouse_event_closure('scroll')(event);\n",
+ " });\n",
+ "\n",
+ " canvas_div.appendChild(canvas);\n",
+ " canvas_div.appendChild(rubberband_canvas);\n",
+ "\n",
+ " this.rubberband_context = rubberband_canvas.getContext('2d');\n",
+ " this.rubberband_context.strokeStyle = '#000000';\n",
+ "\n",
+ " this._resize_canvas = function (width, height, forward) {\n",
+ " if (forward) {\n",
+ " canvas_div.style.width = width + 'px';\n",
+ " canvas_div.style.height = height + 'px';\n",
+ " }\n",
+ " };\n",
+ "\n",
+ " // Disable right mouse context menu.\n",
+ " canvas_div.addEventListener('contextmenu', function (_e) {\n",
+ " event.preventDefault();\n",
+ " return false;\n",
+ " });\n",
+ "\n",
+ " function set_focus() {\n",
+ " canvas.focus();\n",
+ " canvas_div.focus();\n",
+ " }\n",
+ "\n",
+ " window.setTimeout(set_focus, 100);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._init_toolbar = function () {\n",
+ " var fig = this;\n",
+ "\n",
+ " var toolbar = document.createElement('div');\n",
+ " toolbar.classList = 'mpl-toolbar';\n",
+ " this.root.appendChild(toolbar);\n",
+ "\n",
+ " function on_click_closure(name) {\n",
+ " return function (_event) {\n",
+ " return fig.toolbar_button_onclick(name);\n",
+ " };\n",
+ " }\n",
+ "\n",
+ " function on_mouseover_closure(tooltip) {\n",
+ " return function (event) {\n",
+ " if (!event.currentTarget.disabled) {\n",
+ " return fig.toolbar_button_onmouseover(tooltip);\n",
+ " }\n",
+ " };\n",
+ " }\n",
+ "\n",
+ " fig.buttons = {};\n",
+ " var buttonGroup = document.createElement('div');\n",
+ " buttonGroup.classList = 'mpl-button-group';\n",
+ " for (var toolbar_ind in mpl.toolbar_items) {\n",
+ " var name = mpl.toolbar_items[toolbar_ind][0];\n",
+ " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
+ " var image = mpl.toolbar_items[toolbar_ind][2];\n",
+ " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
+ "\n",
+ " if (!name) {\n",
+ " /* Instead of a spacer, we start a new button group. */\n",
+ " if (buttonGroup.hasChildNodes()) {\n",
+ " toolbar.appendChild(buttonGroup);\n",
+ " }\n",
+ " buttonGroup = document.createElement('div');\n",
+ " buttonGroup.classList = 'mpl-button-group';\n",
+ " continue;\n",
+ " }\n",
+ "\n",
+ " var button = (fig.buttons[name] = document.createElement('button'));\n",
+ " button.classList = 'mpl-widget';\n",
+ " button.setAttribute('role', 'button');\n",
+ " button.setAttribute('aria-disabled', 'false');\n",
+ " button.addEventListener('click', on_click_closure(method_name));\n",
+ " button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n",
+ "\n",
+ " var icon_img = document.createElement('img');\n",
+ " icon_img.src = '_images/' + image + '.png';\n",
+ " icon_img.srcset = '_images/' + image + '_large.png 2x';\n",
+ " icon_img.alt = tooltip;\n",
+ " button.appendChild(icon_img);\n",
+ "\n",
+ " buttonGroup.appendChild(button);\n",
+ " }\n",
+ "\n",
+ " if (buttonGroup.hasChildNodes()) {\n",
+ " toolbar.appendChild(buttonGroup);\n",
+ " }\n",
+ "\n",
+ " var fmt_picker = document.createElement('select');\n",
+ " fmt_picker.classList = 'mpl-widget';\n",
+ " toolbar.appendChild(fmt_picker);\n",
+ " this.format_dropdown = fmt_picker;\n",
+ "\n",
+ " for (var ind in mpl.extensions) {\n",
+ " var fmt = mpl.extensions[ind];\n",
+ " var option = document.createElement('option');\n",
+ " option.selected = fmt === mpl.default_extension;\n",
+ " option.innerHTML = fmt;\n",
+ " fmt_picker.appendChild(option);\n",
+ " }\n",
+ "\n",
+ " var status_bar = document.createElement('span');\n",
+ " status_bar.classList = 'mpl-message';\n",
+ " toolbar.appendChild(status_bar);\n",
+ " this.message = status_bar;\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.request_resize = function (x_pixels, y_pixels) {\n",
+ " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n",
+ " // which will in turn request a refresh of the image.\n",
+ " this.send_message('resize', { width: x_pixels, height: y_pixels });\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.send_message = function (type, properties) {\n",
+ " properties['type'] = type;\n",
+ " properties['figure_id'] = this.id;\n",
+ " this.ws.send(JSON.stringify(properties));\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.send_draw_message = function () {\n",
+ " if (!this.waiting) {\n",
+ " this.waiting = true;\n",
+ " this.ws.send(JSON.stringify({ type: 'draw', figure_id: this.id }));\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_save = function (fig, _msg) {\n",
+ " var format_dropdown = fig.format_dropdown;\n",
+ " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n",
+ " fig.ondownload(fig, format);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_resize = function (fig, msg) {\n",
+ " var size = msg['size'];\n",
+ " if (size[0] !== fig.canvas.width || size[1] !== fig.canvas.height) {\n",
+ " fig._resize_canvas(size[0], size[1], msg['forward']);\n",
+ " fig.send_message('refresh', {});\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_rubberband = function (fig, msg) {\n",
+ " var x0 = msg['x0'] / fig.ratio;\n",
+ " var y0 = (fig.canvas.height - msg['y0']) / fig.ratio;\n",
+ " var x1 = msg['x1'] / fig.ratio;\n",
+ " var y1 = (fig.canvas.height - msg['y1']) / fig.ratio;\n",
+ " x0 = Math.floor(x0) + 0.5;\n",
+ " y0 = Math.floor(y0) + 0.5;\n",
+ " x1 = Math.floor(x1) + 0.5;\n",
+ " y1 = Math.floor(y1) + 0.5;\n",
+ " var min_x = Math.min(x0, x1);\n",
+ " var min_y = Math.min(y0, y1);\n",
+ " var width = Math.abs(x1 - x0);\n",
+ " var height = Math.abs(y1 - y0);\n",
+ "\n",
+ " fig.rubberband_context.clearRect(\n",
+ " 0,\n",
+ " 0,\n",
+ " fig.canvas.width / fig.ratio,\n",
+ " fig.canvas.height / fig.ratio\n",
+ " );\n",
+ "\n",
+ " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_figure_label = function (fig, msg) {\n",
+ " // Updates the figure title.\n",
+ " fig.header.textContent = msg['label'];\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_cursor = function (fig, msg) {\n",
+ " fig.canvas_div.style.cursor = msg['cursor'];\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_message = function (fig, msg) {\n",
+ " fig.message.textContent = msg['message'];\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_draw = function (fig, _msg) {\n",
+ " // Request the server to send over a new figure.\n",
+ " fig.send_draw_message();\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_image_mode = function (fig, msg) {\n",
+ " fig.image_mode = msg['mode'];\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_history_buttons = function (fig, msg) {\n",
+ " for (var key in msg) {\n",
+ " if (!(key in fig.buttons)) {\n",
+ " continue;\n",
+ " }\n",
+ " fig.buttons[key].disabled = !msg[key];\n",
+ " fig.buttons[key].setAttribute('aria-disabled', !msg[key]);\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_navigate_mode = function (fig, msg) {\n",
+ " if (msg['mode'] === 'PAN') {\n",
+ " fig.buttons['Pan'].classList.add('active');\n",
+ " fig.buttons['Zoom'].classList.remove('active');\n",
+ " } else if (msg['mode'] === 'ZOOM') {\n",
+ " fig.buttons['Pan'].classList.remove('active');\n",
+ " fig.buttons['Zoom'].classList.add('active');\n",
+ " } else {\n",
+ " fig.buttons['Pan'].classList.remove('active');\n",
+ " fig.buttons['Zoom'].classList.remove('active');\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.updated_canvas_event = function () {\n",
+ " // Called whenever the canvas gets updated.\n",
+ " this.send_message('ack', {});\n",
+ "};\n",
+ "\n",
+ "// A function to construct a web socket function for onmessage handling.\n",
+ "// Called in the figure constructor.\n",
+ "mpl.figure.prototype._make_on_message_function = function (fig) {\n",
+ " return function socket_on_message(evt) {\n",
+ " if (evt.data instanceof Blob) {\n",
+ " var img = evt.data;\n",
+ " if (img.type !== 'image/png') {\n",
+ " /* FIXME: We get \"Resource interpreted as Image but\n",
+ " * transferred with MIME type text/plain:\" errors on\n",
+ " * Chrome. But how to set the MIME type? It doesn't seem\n",
+ " * to be part of the websocket stream */\n",
+ " img.type = 'image/png';\n",
+ " }\n",
+ "\n",
+ " /* Free the memory for the previous frames */\n",
+ " if (fig.imageObj.src) {\n",
+ " (window.URL || window.webkitURL).revokeObjectURL(\n",
+ " fig.imageObj.src\n",
+ " );\n",
+ " }\n",
+ "\n",
+ " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
+ " img\n",
+ " );\n",
+ " fig.updated_canvas_event();\n",
+ " fig.waiting = false;\n",
+ " return;\n",
+ " } else if (\n",
+ " typeof evt.data === 'string' &&\n",
+ " evt.data.slice(0, 21) === 'data:image/png;base64'\n",
+ " ) {\n",
+ " fig.imageObj.src = evt.data;\n",
+ " fig.updated_canvas_event();\n",
+ " fig.waiting = false;\n",
+ " return;\n",
+ " }\n",
+ "\n",
+ " var msg = JSON.parse(evt.data);\n",
+ " var msg_type = msg['type'];\n",
+ "\n",
+ " // Call the \"handle_{type}\" callback, which takes\n",
+ " // the figure and JSON message as its only arguments.\n",
+ " try {\n",
+ " var callback = fig['handle_' + msg_type];\n",
+ " } catch (e) {\n",
+ " console.log(\n",
+ " \"No handler for the '\" + msg_type + \"' message type: \",\n",
+ " msg\n",
+ " );\n",
+ " return;\n",
+ " }\n",
+ "\n",
+ " if (callback) {\n",
+ " try {\n",
+ " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n",
+ " callback(fig, msg);\n",
+ " } catch (e) {\n",
+ " console.log(\n",
+ " \"Exception inside the 'handler_\" + msg_type + \"' callback:\",\n",
+ " e,\n",
+ " e.stack,\n",
+ " msg\n",
+ " );\n",
+ " }\n",
+ " }\n",
+ " };\n",
+ "};\n",
+ "\n",
+ "function getModifiers(event) {\n",
+ " var mods = [];\n",
+ " if (event.ctrlKey) {\n",
+ " mods.push('ctrl');\n",
+ " }\n",
+ " if (event.altKey) {\n",
+ " mods.push('alt');\n",
+ " }\n",
+ " if (event.shiftKey) {\n",
+ " mods.push('shift');\n",
+ " }\n",
+ " if (event.metaKey) {\n",
+ " mods.push('meta');\n",
+ " }\n",
+ " return mods;\n",
+ "}\n",
+ "\n",
+ "/*\n",
+ " * return a copy of an object with only non-object keys\n",
+ " * we need this to avoid circular references\n",
+ " * https://stackoverflow.com/a/24161582/3208463\n",
+ " */\n",
+ "function simpleKeys(original) {\n",
+ " return Object.keys(original).reduce(function (obj, key) {\n",
+ " if (typeof original[key] !== 'object') {\n",
+ " obj[key] = original[key];\n",
+ " }\n",
+ " return obj;\n",
+ " }, {});\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.mouse_event = function (event, name) {\n",
+ " if (name === 'button_press') {\n",
+ " this.canvas.focus();\n",
+ " this.canvas_div.focus();\n",
+ " }\n",
+ "\n",
+ " // from https://stackoverflow.com/q/1114465\n",
+ " var boundingRect = this.canvas.getBoundingClientRect();\n",
+ " var x = (event.clientX - boundingRect.left) * this.ratio;\n",
+ " var y = (event.clientY - boundingRect.top) * this.ratio;\n",
+ "\n",
+ " this.send_message(name, {\n",
+ " x: x,\n",
+ " y: y,\n",
+ " button: event.button,\n",
+ " step: event.step,\n",
+ " modifiers: getModifiers(event),\n",
+ " guiEvent: simpleKeys(event),\n",
+ " });\n",
+ "\n",
+ " return false;\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._key_event_extra = function (_event, _name) {\n",
+ " // Handle any extra behaviour associated with a key event\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.key_event = function (event, name) {\n",
+ " // Prevent repeat events\n",
+ " if (name === 'key_press') {\n",
+ " if (event.key === this._key) {\n",
+ " return;\n",
+ " } else {\n",
+ " this._key = event.key;\n",
+ " }\n",
+ " }\n",
+ " if (name === 'key_release') {\n",
+ " this._key = null;\n",
+ " }\n",
+ "\n",
+ " var value = '';\n",
+ " if (event.ctrlKey && event.key !== 'Control') {\n",
+ " value += 'ctrl+';\n",
+ " }\n",
+ " else if (event.altKey && event.key !== 'Alt') {\n",
+ " value += 'alt+';\n",
+ " }\n",
+ " else if (event.shiftKey && event.key !== 'Shift') {\n",
+ " value += 'shift+';\n",
+ " }\n",
+ "\n",
+ " value += 'k' + event.key;\n",
+ "\n",
+ " this._key_event_extra(event, name);\n",
+ "\n",
+ " this.send_message(name, { key: value, guiEvent: simpleKeys(event) });\n",
+ " return false;\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.toolbar_button_onclick = function (name) {\n",
+ " if (name === 'download') {\n",
+ " this.handle_save(this, null);\n",
+ " } else {\n",
+ " this.send_message('toolbar_button', { name: name });\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.toolbar_button_onmouseover = function (tooltip) {\n",
+ " this.message.textContent = tooltip;\n",
+ "};\n",
+ "\n",
+ "///////////////// REMAINING CONTENT GENERATED BY embed_js.py /////////////////\n",
+ "// prettier-ignore\n",
+ "var _JSXTOOLS_RESIZE_OBSERVER=function(A){var t,i=new WeakMap,n=new WeakMap,a=new WeakMap,r=new WeakMap,o=new Set;function s(e){if(!(this instanceof s))throw new TypeError(\"Constructor requires 'new' operator\");i.set(this,e)}function h(){throw new TypeError(\"Function is not a constructor\")}function c(e,t,i,n){e=0 in arguments?Number(arguments[0]):0,t=1 in arguments?Number(arguments[1]):0,i=2 in arguments?Number(arguments[2]):0,n=3 in arguments?Number(arguments[3]):0,this.right=(this.x=this.left=e)+(this.width=i),this.bottom=(this.y=this.top=t)+(this.height=n),Object.freeze(this)}function d(){t=requestAnimationFrame(d);var s=new WeakMap,p=new Set;o.forEach((function(t){r.get(t).forEach((function(i){var r=t instanceof window.SVGElement,o=a.get(t),d=r?0:parseFloat(o.paddingTop),f=r?0:parseFloat(o.paddingRight),l=r?0:parseFloat(o.paddingBottom),u=r?0:parseFloat(o.paddingLeft),g=r?0:parseFloat(o.borderTopWidth),m=r?0:parseFloat(o.borderRightWidth),w=r?0:parseFloat(o.borderBottomWidth),b=u+f,F=d+l,v=(r?0:parseFloat(o.borderLeftWidth))+m,W=g+w,y=r?0:t.offsetHeight-W-t.clientHeight,E=r?0:t.offsetWidth-v-t.clientWidth,R=b+v,z=F+W,M=r?t.width:parseFloat(o.width)-R-E,O=r?t.height:parseFloat(o.height)-z-y;if(n.has(t)){var k=n.get(t);if(k[0]===M&&k[1]===O)return}n.set(t,[M,O]);var S=Object.create(h.prototype);S.target=t,S.contentRect=new c(u,d,M,O),s.has(i)||(s.set(i,[]),p.add(i)),s.get(i).push(S)}))})),p.forEach((function(e){i.get(e).call(e,s.get(e),e)}))}return s.prototype.observe=function(i){if(i instanceof window.Element){r.has(i)||(r.set(i,new Set),o.add(i),a.set(i,window.getComputedStyle(i)));var n=r.get(i);n.has(this)||n.add(this),cancelAnimationFrame(t),t=requestAnimationFrame(d)}},s.prototype.unobserve=function(i){if(i instanceof window.Element&&r.has(i)){var n=r.get(i);n.has(this)&&(n.delete(this),n.size||(r.delete(i),o.delete(i))),n.size||r.delete(i),o.size||cancelAnimationFrame(t)}},A.DOMRectReadOnly=c,A.ResizeObserver=s,A.ResizeObserverEntry=h,A}; // eslint-disable-line\n",
+ "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Left button pans, Right button zooms\\nx/y fixes axis, CTRL fixes aspect\", \"fa fa-arrows\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\\nx/y fixes axis\", \"fa fa-square-o\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o\", \"download\"]];\n",
+ "\n",
+ "mpl.extensions = [\"eps\", \"jpeg\", \"pgf\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\", \"tif\", \"webp\"];\n",
+ "\n",
+ "mpl.default_extension = \"png\";/* global mpl */\n",
+ "\n",
+ "var comm_websocket_adapter = function (comm) {\n",
+ " // Create a \"websocket\"-like object which calls the given IPython comm\n",
+ " // object with the appropriate methods. Currently this is a non binary\n",
+ " // socket, so there is still some room for performance tuning.\n",
+ " var ws = {};\n",
+ "\n",
+ " ws.binaryType = comm.kernel.ws.binaryType;\n",
+ " ws.readyState = comm.kernel.ws.readyState;\n",
+ " function updateReadyState(_event) {\n",
+ " if (comm.kernel.ws) {\n",
+ " ws.readyState = comm.kernel.ws.readyState;\n",
+ " } else {\n",
+ " ws.readyState = 3; // Closed state.\n",
+ " }\n",
+ " }\n",
+ " comm.kernel.ws.addEventListener('open', updateReadyState);\n",
+ " comm.kernel.ws.addEventListener('close', updateReadyState);\n",
+ " comm.kernel.ws.addEventListener('error', updateReadyState);\n",
+ "\n",
+ " ws.close = function () {\n",
+ " comm.close();\n",
+ " };\n",
+ " ws.send = function (m) {\n",
+ " //console.log('sending', m);\n",
+ " comm.send(m);\n",
+ " };\n",
+ " // Register the callback with on_msg.\n",
+ " comm.on_msg(function (msg) {\n",
+ " //console.log('receiving', msg['content']['data'], msg);\n",
+ " var data = msg['content']['data'];\n",
+ " if (data['blob'] !== undefined) {\n",
+ " data = {\n",
+ " data: new Blob(msg['buffers'], { type: data['blob'] }),\n",
+ " };\n",
+ " }\n",
+ " // Pass the mpl event to the overridden (by mpl) onmessage function.\n",
+ " ws.onmessage(data);\n",
+ " });\n",
+ " return ws;\n",
+ "};\n",
+ "\n",
+ "mpl.mpl_figure_comm = function (comm, msg) {\n",
+ " // This is the function which gets called when the mpl process\n",
+ " // starts-up an IPython Comm through the \"matplotlib\" channel.\n",
+ "\n",
+ " var id = msg.content.data.id;\n",
+ " // Get hold of the div created by the display call when the Comm\n",
+ " // socket was opened in Python.\n",
+ " var element = document.getElementById(id);\n",
+ " var ws_proxy = comm_websocket_adapter(comm);\n",
+ "\n",
+ " function ondownload(figure, _format) {\n",
+ " window.open(figure.canvas.toDataURL());\n",
+ " }\n",
+ "\n",
+ " var fig = new mpl.figure(id, ws_proxy, ondownload, element);\n",
+ "\n",
+ " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n",
+ " // web socket which is closed, not our websocket->open comm proxy.\n",
+ " ws_proxy.onopen();\n",
+ "\n",
+ " fig.parent_element = element;\n",
+ " fig.cell_info = mpl.find_output_cell(\"\");\n",
+ " if (!fig.cell_info) {\n",
+ " console.error('Failed to find cell for figure', id, fig);\n",
+ " return;\n",
+ " }\n",
+ " fig.cell_info[0].output_area.element.on(\n",
+ " 'cleared',\n",
+ " { fig: fig },\n",
+ " fig._remove_fig_handler\n",
+ " );\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_close = function (fig, msg) {\n",
+ " var width = fig.canvas.width / fig.ratio;\n",
+ " fig.cell_info[0].output_area.element.off(\n",
+ " 'cleared',\n",
+ " fig._remove_fig_handler\n",
+ " );\n",
+ " fig.resizeObserverInstance.unobserve(fig.canvas_div);\n",
+ "\n",
+ " // Update the output cell to use the data from the current canvas.\n",
+ " fig.push_to_output();\n",
+ " var dataURL = fig.canvas.toDataURL();\n",
+ " // Re-enable the keyboard manager in IPython - without this line, in FF,\n",
+ " // the notebook keyboard shortcuts fail.\n",
+ " IPython.keyboard_manager.enable();\n",
+ " fig.parent_element.innerHTML =\n",
+ " '';\n",
+ " fig.close_ws(fig, msg);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.close_ws = function (fig, msg) {\n",
+ " fig.send_message('closing', msg);\n",
+ " // fig.ws.close()\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.push_to_output = function (_remove_interactive) {\n",
+ " // Turn the data on the canvas into data in the output cell.\n",
+ " var width = this.canvas.width / this.ratio;\n",
+ " var dataURL = this.canvas.toDataURL();\n",
+ " this.cell_info[1]['text/html'] =\n",
+ " '';\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.updated_canvas_event = function () {\n",
+ " // Tell IPython that the notebook contents must change.\n",
+ " IPython.notebook.set_dirty(true);\n",
+ " this.send_message('ack', {});\n",
+ " var fig = this;\n",
+ " // Wait a second, then push the new image to the DOM so\n",
+ " // that it is saved nicely (might be nice to debounce this).\n",
+ " setTimeout(function () {\n",
+ " fig.push_to_output();\n",
+ " }, 1000);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._init_toolbar = function () {\n",
+ " var fig = this;\n",
+ "\n",
+ " var toolbar = document.createElement('div');\n",
+ " toolbar.classList = 'btn-toolbar';\n",
+ " this.root.appendChild(toolbar);\n",
+ "\n",
+ " function on_click_closure(name) {\n",
+ " return function (_event) {\n",
+ " return fig.toolbar_button_onclick(name);\n",
+ " };\n",
+ " }\n",
+ "\n",
+ " function on_mouseover_closure(tooltip) {\n",
+ " return function (event) {\n",
+ " if (!event.currentTarget.disabled) {\n",
+ " return fig.toolbar_button_onmouseover(tooltip);\n",
+ " }\n",
+ " };\n",
+ " }\n",
+ "\n",
+ " fig.buttons = {};\n",
+ " var buttonGroup = document.createElement('div');\n",
+ " buttonGroup.classList = 'btn-group';\n",
+ " var button;\n",
+ " for (var toolbar_ind in mpl.toolbar_items) {\n",
+ " var name = mpl.toolbar_items[toolbar_ind][0];\n",
+ " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
+ " var image = mpl.toolbar_items[toolbar_ind][2];\n",
+ " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
+ "\n",
+ " if (!name) {\n",
+ " /* Instead of a spacer, we start a new button group. */\n",
+ " if (buttonGroup.hasChildNodes()) {\n",
+ " toolbar.appendChild(buttonGroup);\n",
+ " }\n",
+ " buttonGroup = document.createElement('div');\n",
+ " buttonGroup.classList = 'btn-group';\n",
+ " continue;\n",
+ " }\n",
+ "\n",
+ " button = fig.buttons[name] = document.createElement('button');\n",
+ " button.classList = 'btn btn-default';\n",
+ " button.href = '#';\n",
+ " button.title = name;\n",
+ " button.innerHTML = '';\n",
+ " button.addEventListener('click', on_click_closure(method_name));\n",
+ " button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n",
+ " buttonGroup.appendChild(button);\n",
+ " }\n",
+ "\n",
+ " if (buttonGroup.hasChildNodes()) {\n",
+ " toolbar.appendChild(buttonGroup);\n",
+ " }\n",
+ "\n",
+ " // Add the status bar.\n",
+ " var status_bar = document.createElement('span');\n",
+ " status_bar.classList = 'mpl-message pull-right';\n",
+ " toolbar.appendChild(status_bar);\n",
+ " this.message = status_bar;\n",
+ "\n",
+ " // Add the close button to the window.\n",
+ " var buttongrp = document.createElement('div');\n",
+ " buttongrp.classList = 'btn-group inline pull-right';\n",
+ " button = document.createElement('button');\n",
+ " button.classList = 'btn btn-mini btn-primary';\n",
+ " button.href = '#';\n",
+ " button.title = 'Stop Interaction';\n",
+ " button.innerHTML = '';\n",
+ " button.addEventListener('click', function (_evt) {\n",
+ " fig.handle_close(fig, {});\n",
+ " });\n",
+ " button.addEventListener(\n",
+ " 'mouseover',\n",
+ " on_mouseover_closure('Stop Interaction')\n",
+ " );\n",
+ " buttongrp.appendChild(button);\n",
+ " var titlebar = this.root.querySelector('.ui-dialog-titlebar');\n",
+ " titlebar.insertBefore(buttongrp, titlebar.firstChild);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._remove_fig_handler = function (event) {\n",
+ " var fig = event.data.fig;\n",
+ " if (event.target !== this) {\n",
+ " // Ignore bubbled events from children.\n",
+ " return;\n",
+ " }\n",
+ " fig.close_ws(fig, {});\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._root_extra_style = function (el) {\n",
+ " el.style.boxSizing = 'content-box'; // override notebook setting of border-box.\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._canvas_extra_style = function (el) {\n",
+ " // this is important to make the div 'focusable\n",
+ " el.setAttribute('tabindex', 0);\n",
+ " // reach out to IPython and tell the keyboard manager to turn it's self\n",
+ " // off when our div gets focus\n",
+ "\n",
+ " // location in version 3\n",
+ " if (IPython.notebook.keyboard_manager) {\n",
+ " IPython.notebook.keyboard_manager.register_events(el);\n",
+ " } else {\n",
+ " // location in version 2\n",
+ " IPython.keyboard_manager.register_events(el);\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._key_event_extra = function (event, _name) {\n",
+ " // Check for shift+enter\n",
+ " if (event.shiftKey && event.which === 13) {\n",
+ " this.canvas_div.blur();\n",
+ " // select the cell after this one\n",
+ " var index = IPython.notebook.find_cell_index(this.cell_info[0]);\n",
+ " IPython.notebook.select(index + 1);\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_save = function (fig, _msg) {\n",
+ " fig.ondownload(fig, null);\n",
+ "};\n",
+ "\n",
+ "mpl.find_output_cell = function (html_output) {\n",
+ " // Return the cell and output element which can be found *uniquely* in the notebook.\n",
+ " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n",
+ " // IPython event is triggered only after the cells have been serialised, which for\n",
+ " // our purposes (turning an active figure into a static one), is too late.\n",
+ " var cells = IPython.notebook.get_cells();\n",
+ " var ncells = cells.length;\n",
+ " for (var i = 0; i < ncells; i++) {\n",
+ " var cell = cells[i];\n",
+ " if (cell.cell_type === 'code') {\n",
+ " for (var j = 0; j < cell.output_area.outputs.length; j++) {\n",
+ " var data = cell.output_area.outputs[j];\n",
+ " if (data.data) {\n",
+ " // IPython >= 3 moved mimebundle to data attribute of output\n",
+ " data = data.data;\n",
+ " }\n",
+ " if (data['text/html'] === html_output) {\n",
+ " return [cell, data, j];\n",
+ " }\n",
+ " }\n",
+ " }\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "// Register the function which deals with the matplotlib target/channel.\n",
+ "// The kernel may be null if the page has been refreshed.\n",
+ "if (IPython.notebook.kernel !== null) {\n",
+ " IPython.notebook.kernel.comm_manager.register_target(\n",
+ " 'matplotlib',\n",
+ " mpl.mpl_figure_comm\n",
+ " );\n",
+ "}\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/html": [
+ ""
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "EnKF (replay): 100%|███████████| 3000/3000 [00:00<00:00, 15191.34it/s]\n"
+ ]
+ }
+ ],
+ "source": [
+ "xp.stats.replay(\n",
+ " # speed=.6 # `speed` does not work in notebooks\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "6335abba",
+ "metadata": {},
+ "source": [
+ "### Further diagnostic plots"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "id": "da5fd628",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import dapper.tools.viz as viz"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "id": "450a7fc1",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "application/javascript": [
+ "/* Put everything inside the global mpl namespace */\n",
+ "/* global mpl */\n",
+ "window.mpl = {};\n",
+ "\n",
+ "mpl.get_websocket_type = function () {\n",
+ " if (typeof WebSocket !== 'undefined') {\n",
+ " return WebSocket;\n",
+ " } else if (typeof MozWebSocket !== 'undefined') {\n",
+ " return MozWebSocket;\n",
+ " } else {\n",
+ " alert(\n",
+ " 'Your browser does not have WebSocket support. ' +\n",
+ " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n",
+ " 'Firefox 4 and 5 are also supported but you ' +\n",
+ " 'have to enable WebSockets in about:config.'\n",
+ " );\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure = function (figure_id, websocket, ondownload, parent_element) {\n",
+ " this.id = figure_id;\n",
+ "\n",
+ " this.ws = websocket;\n",
+ "\n",
+ " this.supports_binary = this.ws.binaryType !== undefined;\n",
+ "\n",
+ " if (!this.supports_binary) {\n",
+ " var warnings = document.getElementById('mpl-warnings');\n",
+ " if (warnings) {\n",
+ " warnings.style.display = 'block';\n",
+ " warnings.textContent =\n",
+ " 'This browser does not support binary websocket messages. ' +\n",
+ " 'Performance may be slow.';\n",
+ " }\n",
+ " }\n",
+ "\n",
+ " this.imageObj = new Image();\n",
+ "\n",
+ " this.context = undefined;\n",
+ " this.message = undefined;\n",
+ " this.canvas = undefined;\n",
+ " this.rubberband_canvas = undefined;\n",
+ " this.rubberband_context = undefined;\n",
+ " this.format_dropdown = undefined;\n",
+ "\n",
+ " this.image_mode = 'full';\n",
+ "\n",
+ " this.root = document.createElement('div');\n",
+ " this.root.setAttribute('style', 'display: inline-block');\n",
+ " this._root_extra_style(this.root);\n",
+ "\n",
+ " parent_element.appendChild(this.root);\n",
+ "\n",
+ " this._init_header(this);\n",
+ " this._init_canvas(this);\n",
+ " this._init_toolbar(this);\n",
+ "\n",
+ " var fig = this;\n",
+ "\n",
+ " this.waiting = false;\n",
+ "\n",
+ " this.ws.onopen = function () {\n",
+ " fig.send_message('supports_binary', { value: fig.supports_binary });\n",
+ " fig.send_message('send_image_mode', {});\n",
+ " if (fig.ratio !== 1) {\n",
+ " fig.send_message('set_device_pixel_ratio', {\n",
+ " device_pixel_ratio: fig.ratio,\n",
+ " });\n",
+ " }\n",
+ " fig.send_message('refresh', {});\n",
+ " };\n",
+ "\n",
+ " this.imageObj.onload = function () {\n",
+ " if (fig.image_mode === 'full') {\n",
+ " // Full images could contain transparency (where diff images\n",
+ " // almost always do), so we need to clear the canvas so that\n",
+ " // there is no ghosting.\n",
+ " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n",
+ " }\n",
+ " fig.context.drawImage(fig.imageObj, 0, 0);\n",
+ " };\n",
+ "\n",
+ " this.imageObj.onunload = function () {\n",
+ " fig.ws.close();\n",
+ " };\n",
+ "\n",
+ " this.ws.onmessage = this._make_on_message_function(this);\n",
+ "\n",
+ " this.ondownload = ondownload;\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._init_header = function () {\n",
+ " var titlebar = document.createElement('div');\n",
+ " titlebar.classList =\n",
+ " 'ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix';\n",
+ " var titletext = document.createElement('div');\n",
+ " titletext.classList = 'ui-dialog-title';\n",
+ " titletext.setAttribute(\n",
+ " 'style',\n",
+ " 'width: 100%; text-align: center; padding: 3px;'\n",
+ " );\n",
+ " titlebar.appendChild(titletext);\n",
+ " this.root.appendChild(titlebar);\n",
+ " this.header = titletext;\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._canvas_extra_style = function (_canvas_div) {};\n",
+ "\n",
+ "mpl.figure.prototype._root_extra_style = function (_canvas_div) {};\n",
+ "\n",
+ "mpl.figure.prototype._init_canvas = function () {\n",
+ " var fig = this;\n",
+ "\n",
+ " var canvas_div = (this.canvas_div = document.createElement('div'));\n",
+ " canvas_div.setAttribute('tabindex', '0');\n",
+ " canvas_div.setAttribute(\n",
+ " 'style',\n",
+ " 'border: 1px solid #ddd;' +\n",
+ " 'box-sizing: content-box;' +\n",
+ " 'clear: both;' +\n",
+ " 'min-height: 1px;' +\n",
+ " 'min-width: 1px;' +\n",
+ " 'outline: 0;' +\n",
+ " 'overflow: hidden;' +\n",
+ " 'position: relative;' +\n",
+ " 'resize: both;' +\n",
+ " 'z-index: 2;'\n",
+ " );\n",
+ "\n",
+ " function on_keyboard_event_closure(name) {\n",
+ " return function (event) {\n",
+ " return fig.key_event(event, name);\n",
+ " };\n",
+ " }\n",
+ "\n",
+ " canvas_div.addEventListener(\n",
+ " 'keydown',\n",
+ " on_keyboard_event_closure('key_press')\n",
+ " );\n",
+ " canvas_div.addEventListener(\n",
+ " 'keyup',\n",
+ " on_keyboard_event_closure('key_release')\n",
+ " );\n",
+ "\n",
+ " this._canvas_extra_style(canvas_div);\n",
+ " this.root.appendChild(canvas_div);\n",
+ "\n",
+ " var canvas = (this.canvas = document.createElement('canvas'));\n",
+ " canvas.classList.add('mpl-canvas');\n",
+ " canvas.setAttribute(\n",
+ " 'style',\n",
+ " 'box-sizing: content-box;' +\n",
+ " 'pointer-events: none;' +\n",
+ " 'position: relative;' +\n",
+ " 'z-index: 0;'\n",
+ " );\n",
+ "\n",
+ " this.context = canvas.getContext('2d');\n",
+ "\n",
+ " var backingStore =\n",
+ " this.context.backingStorePixelRatio ||\n",
+ " this.context.webkitBackingStorePixelRatio ||\n",
+ " this.context.mozBackingStorePixelRatio ||\n",
+ " this.context.msBackingStorePixelRatio ||\n",
+ " this.context.oBackingStorePixelRatio ||\n",
+ " this.context.backingStorePixelRatio ||\n",
+ " 1;\n",
+ "\n",
+ " this.ratio = (window.devicePixelRatio || 1) / backingStore;\n",
+ "\n",
+ " var rubberband_canvas = (this.rubberband_canvas = document.createElement(\n",
+ " 'canvas'\n",
+ " ));\n",
+ " rubberband_canvas.setAttribute(\n",
+ " 'style',\n",
+ " 'box-sizing: content-box;' +\n",
+ " 'left: 0;' +\n",
+ " 'pointer-events: none;' +\n",
+ " 'position: absolute;' +\n",
+ " 'top: 0;' +\n",
+ " 'z-index: 1;'\n",
+ " );\n",
+ "\n",
+ " // Apply a ponyfill if ResizeObserver is not implemented by browser.\n",
+ " if (this.ResizeObserver === undefined) {\n",
+ " if (window.ResizeObserver !== undefined) {\n",
+ " this.ResizeObserver = window.ResizeObserver;\n",
+ " } else {\n",
+ " var obs = _JSXTOOLS_RESIZE_OBSERVER({});\n",
+ " this.ResizeObserver = obs.ResizeObserver;\n",
+ " }\n",
+ " }\n",
+ "\n",
+ " this.resizeObserverInstance = new this.ResizeObserver(function (entries) {\n",
+ " var nentries = entries.length;\n",
+ " for (var i = 0; i < nentries; i++) {\n",
+ " var entry = entries[i];\n",
+ " var width, height;\n",
+ " if (entry.contentBoxSize) {\n",
+ " if (entry.contentBoxSize instanceof Array) {\n",
+ " // Chrome 84 implements new version of spec.\n",
+ " width = entry.contentBoxSize[0].inlineSize;\n",
+ " height = entry.contentBoxSize[0].blockSize;\n",
+ " } else {\n",
+ " // Firefox implements old version of spec.\n",
+ " width = entry.contentBoxSize.inlineSize;\n",
+ " height = entry.contentBoxSize.blockSize;\n",
+ " }\n",
+ " } else {\n",
+ " // Chrome <84 implements even older version of spec.\n",
+ " width = entry.contentRect.width;\n",
+ " height = entry.contentRect.height;\n",
+ " }\n",
+ "\n",
+ " // Keep the size of the canvas and rubber band canvas in sync with\n",
+ " // the canvas container.\n",
+ " if (entry.devicePixelContentBoxSize) {\n",
+ " // Chrome 84 implements new version of spec.\n",
+ " canvas.setAttribute(\n",
+ " 'width',\n",
+ " entry.devicePixelContentBoxSize[0].inlineSize\n",
+ " );\n",
+ " canvas.setAttribute(\n",
+ " 'height',\n",
+ " entry.devicePixelContentBoxSize[0].blockSize\n",
+ " );\n",
+ " } else {\n",
+ " canvas.setAttribute('width', width * fig.ratio);\n",
+ " canvas.setAttribute('height', height * fig.ratio);\n",
+ " }\n",
+ " /* This rescales the canvas back to display pixels, so that it\n",
+ " * appears correct on HiDPI screens. */\n",
+ " canvas.style.width = width + 'px';\n",
+ " canvas.style.height = height + 'px';\n",
+ "\n",
+ " rubberband_canvas.setAttribute('width', width);\n",
+ " rubberband_canvas.setAttribute('height', height);\n",
+ "\n",
+ " // And update the size in Python. We ignore the initial 0/0 size\n",
+ " // that occurs as the element is placed into the DOM, which should\n",
+ " // otherwise not happen due to the minimum size styling.\n",
+ " if (fig.ws.readyState == 1 && width != 0 && height != 0) {\n",
+ " fig.request_resize(width, height);\n",
+ " }\n",
+ " }\n",
+ " });\n",
+ " this.resizeObserverInstance.observe(canvas_div);\n",
+ "\n",
+ " function on_mouse_event_closure(name) {\n",
+ " /* User Agent sniffing is bad, but WebKit is busted:\n",
+ " * https://bugs.webkit.org/show_bug.cgi?id=144526\n",
+ " * https://bugs.webkit.org/show_bug.cgi?id=181818\n",
+ " * The worst that happens here is that they get an extra browser\n",
+ " * selection when dragging, if this check fails to catch them.\n",
+ " */\n",
+ " var UA = navigator.userAgent;\n",
+ " var isWebKit = /AppleWebKit/.test(UA) && !/Chrome/.test(UA);\n",
+ " if(isWebKit) {\n",
+ " return function (event) {\n",
+ " /* This prevents the web browser from automatically changing to\n",
+ " * the text insertion cursor when the button is pressed. We\n",
+ " * want to control all of the cursor setting manually through\n",
+ " * the 'cursor' event from matplotlib */\n",
+ " event.preventDefault()\n",
+ " return fig.mouse_event(event, name);\n",
+ " };\n",
+ " } else {\n",
+ " return function (event) {\n",
+ " return fig.mouse_event(event, name);\n",
+ " };\n",
+ " }\n",
+ " }\n",
+ "\n",
+ " canvas_div.addEventListener(\n",
+ " 'mousedown',\n",
+ " on_mouse_event_closure('button_press')\n",
+ " );\n",
+ " canvas_div.addEventListener(\n",
+ " 'mouseup',\n",
+ " on_mouse_event_closure('button_release')\n",
+ " );\n",
+ " canvas_div.addEventListener(\n",
+ " 'dblclick',\n",
+ " on_mouse_event_closure('dblclick')\n",
+ " );\n",
+ " // Throttle sequential mouse events to 1 every 20ms.\n",
+ " canvas_div.addEventListener(\n",
+ " 'mousemove',\n",
+ " on_mouse_event_closure('motion_notify')\n",
+ " );\n",
+ "\n",
+ " canvas_div.addEventListener(\n",
+ " 'mouseenter',\n",
+ " on_mouse_event_closure('figure_enter')\n",
+ " );\n",
+ " canvas_div.addEventListener(\n",
+ " 'mouseleave',\n",
+ " on_mouse_event_closure('figure_leave')\n",
+ " );\n",
+ "\n",
+ " canvas_div.addEventListener('wheel', function (event) {\n",
+ " if (event.deltaY < 0) {\n",
+ " event.step = 1;\n",
+ " } else {\n",
+ " event.step = -1;\n",
+ " }\n",
+ " on_mouse_event_closure('scroll')(event);\n",
+ " });\n",
+ "\n",
+ " canvas_div.appendChild(canvas);\n",
+ " canvas_div.appendChild(rubberband_canvas);\n",
+ "\n",
+ " this.rubberband_context = rubberband_canvas.getContext('2d');\n",
+ " this.rubberband_context.strokeStyle = '#000000';\n",
+ "\n",
+ " this._resize_canvas = function (width, height, forward) {\n",
+ " if (forward) {\n",
+ " canvas_div.style.width = width + 'px';\n",
+ " canvas_div.style.height = height + 'px';\n",
+ " }\n",
+ " };\n",
+ "\n",
+ " // Disable right mouse context menu.\n",
+ " canvas_div.addEventListener('contextmenu', function (_e) {\n",
+ " event.preventDefault();\n",
+ " return false;\n",
+ " });\n",
+ "\n",
+ " function set_focus() {\n",
+ " canvas.focus();\n",
+ " canvas_div.focus();\n",
+ " }\n",
+ "\n",
+ " window.setTimeout(set_focus, 100);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._init_toolbar = function () {\n",
+ " var fig = this;\n",
+ "\n",
+ " var toolbar = document.createElement('div');\n",
+ " toolbar.classList = 'mpl-toolbar';\n",
+ " this.root.appendChild(toolbar);\n",
+ "\n",
+ " function on_click_closure(name) {\n",
+ " return function (_event) {\n",
+ " return fig.toolbar_button_onclick(name);\n",
+ " };\n",
+ " }\n",
+ "\n",
+ " function on_mouseover_closure(tooltip) {\n",
+ " return function (event) {\n",
+ " if (!event.currentTarget.disabled) {\n",
+ " return fig.toolbar_button_onmouseover(tooltip);\n",
+ " }\n",
+ " };\n",
+ " }\n",
+ "\n",
+ " fig.buttons = {};\n",
+ " var buttonGroup = document.createElement('div');\n",
+ " buttonGroup.classList = 'mpl-button-group';\n",
+ " for (var toolbar_ind in mpl.toolbar_items) {\n",
+ " var name = mpl.toolbar_items[toolbar_ind][0];\n",
+ " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
+ " var image = mpl.toolbar_items[toolbar_ind][2];\n",
+ " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
+ "\n",
+ " if (!name) {\n",
+ " /* Instead of a spacer, we start a new button group. */\n",
+ " if (buttonGroup.hasChildNodes()) {\n",
+ " toolbar.appendChild(buttonGroup);\n",
+ " }\n",
+ " buttonGroup = document.createElement('div');\n",
+ " buttonGroup.classList = 'mpl-button-group';\n",
+ " continue;\n",
+ " }\n",
+ "\n",
+ " var button = (fig.buttons[name] = document.createElement('button'));\n",
+ " button.classList = 'mpl-widget';\n",
+ " button.setAttribute('role', 'button');\n",
+ " button.setAttribute('aria-disabled', 'false');\n",
+ " button.addEventListener('click', on_click_closure(method_name));\n",
+ " button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n",
+ "\n",
+ " var icon_img = document.createElement('img');\n",
+ " icon_img.src = '_images/' + image + '.png';\n",
+ " icon_img.srcset = '_images/' + image + '_large.png 2x';\n",
+ " icon_img.alt = tooltip;\n",
+ " button.appendChild(icon_img);\n",
+ "\n",
+ " buttonGroup.appendChild(button);\n",
+ " }\n",
+ "\n",
+ " if (buttonGroup.hasChildNodes()) {\n",
+ " toolbar.appendChild(buttonGroup);\n",
+ " }\n",
+ "\n",
+ " var fmt_picker = document.createElement('select');\n",
+ " fmt_picker.classList = 'mpl-widget';\n",
+ " toolbar.appendChild(fmt_picker);\n",
+ " this.format_dropdown = fmt_picker;\n",
+ "\n",
+ " for (var ind in mpl.extensions) {\n",
+ " var fmt = mpl.extensions[ind];\n",
+ " var option = document.createElement('option');\n",
+ " option.selected = fmt === mpl.default_extension;\n",
+ " option.innerHTML = fmt;\n",
+ " fmt_picker.appendChild(option);\n",
+ " }\n",
+ "\n",
+ " var status_bar = document.createElement('span');\n",
+ " status_bar.classList = 'mpl-message';\n",
+ " toolbar.appendChild(status_bar);\n",
+ " this.message = status_bar;\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.request_resize = function (x_pixels, y_pixels) {\n",
+ " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n",
+ " // which will in turn request a refresh of the image.\n",
+ " this.send_message('resize', { width: x_pixels, height: y_pixels });\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.send_message = function (type, properties) {\n",
+ " properties['type'] = type;\n",
+ " properties['figure_id'] = this.id;\n",
+ " this.ws.send(JSON.stringify(properties));\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.send_draw_message = function () {\n",
+ " if (!this.waiting) {\n",
+ " this.waiting = true;\n",
+ " this.ws.send(JSON.stringify({ type: 'draw', figure_id: this.id }));\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_save = function (fig, _msg) {\n",
+ " var format_dropdown = fig.format_dropdown;\n",
+ " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n",
+ " fig.ondownload(fig, format);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_resize = function (fig, msg) {\n",
+ " var size = msg['size'];\n",
+ " if (size[0] !== fig.canvas.width || size[1] !== fig.canvas.height) {\n",
+ " fig._resize_canvas(size[0], size[1], msg['forward']);\n",
+ " fig.send_message('refresh', {});\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_rubberband = function (fig, msg) {\n",
+ " var x0 = msg['x0'] / fig.ratio;\n",
+ " var y0 = (fig.canvas.height - msg['y0']) / fig.ratio;\n",
+ " var x1 = msg['x1'] / fig.ratio;\n",
+ " var y1 = (fig.canvas.height - msg['y1']) / fig.ratio;\n",
+ " x0 = Math.floor(x0) + 0.5;\n",
+ " y0 = Math.floor(y0) + 0.5;\n",
+ " x1 = Math.floor(x1) + 0.5;\n",
+ " y1 = Math.floor(y1) + 0.5;\n",
+ " var min_x = Math.min(x0, x1);\n",
+ " var min_y = Math.min(y0, y1);\n",
+ " var width = Math.abs(x1 - x0);\n",
+ " var height = Math.abs(y1 - y0);\n",
+ "\n",
+ " fig.rubberband_context.clearRect(\n",
+ " 0,\n",
+ " 0,\n",
+ " fig.canvas.width / fig.ratio,\n",
+ " fig.canvas.height / fig.ratio\n",
+ " );\n",
+ "\n",
+ " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_figure_label = function (fig, msg) {\n",
+ " // Updates the figure title.\n",
+ " fig.header.textContent = msg['label'];\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_cursor = function (fig, msg) {\n",
+ " fig.canvas_div.style.cursor = msg['cursor'];\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_message = function (fig, msg) {\n",
+ " fig.message.textContent = msg['message'];\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_draw = function (fig, _msg) {\n",
+ " // Request the server to send over a new figure.\n",
+ " fig.send_draw_message();\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_image_mode = function (fig, msg) {\n",
+ " fig.image_mode = msg['mode'];\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_history_buttons = function (fig, msg) {\n",
+ " for (var key in msg) {\n",
+ " if (!(key in fig.buttons)) {\n",
+ " continue;\n",
+ " }\n",
+ " fig.buttons[key].disabled = !msg[key];\n",
+ " fig.buttons[key].setAttribute('aria-disabled', !msg[key]);\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_navigate_mode = function (fig, msg) {\n",
+ " if (msg['mode'] === 'PAN') {\n",
+ " fig.buttons['Pan'].classList.add('active');\n",
+ " fig.buttons['Zoom'].classList.remove('active');\n",
+ " } else if (msg['mode'] === 'ZOOM') {\n",
+ " fig.buttons['Pan'].classList.remove('active');\n",
+ " fig.buttons['Zoom'].classList.add('active');\n",
+ " } else {\n",
+ " fig.buttons['Pan'].classList.remove('active');\n",
+ " fig.buttons['Zoom'].classList.remove('active');\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.updated_canvas_event = function () {\n",
+ " // Called whenever the canvas gets updated.\n",
+ " this.send_message('ack', {});\n",
+ "};\n",
+ "\n",
+ "// A function to construct a web socket function for onmessage handling.\n",
+ "// Called in the figure constructor.\n",
+ "mpl.figure.prototype._make_on_message_function = function (fig) {\n",
+ " return function socket_on_message(evt) {\n",
+ " if (evt.data instanceof Blob) {\n",
+ " var img = evt.data;\n",
+ " if (img.type !== 'image/png') {\n",
+ " /* FIXME: We get \"Resource interpreted as Image but\n",
+ " * transferred with MIME type text/plain:\" errors on\n",
+ " * Chrome. But how to set the MIME type? It doesn't seem\n",
+ " * to be part of the websocket stream */\n",
+ " img.type = 'image/png';\n",
+ " }\n",
+ "\n",
+ " /* Free the memory for the previous frames */\n",
+ " if (fig.imageObj.src) {\n",
+ " (window.URL || window.webkitURL).revokeObjectURL(\n",
+ " fig.imageObj.src\n",
+ " );\n",
+ " }\n",
+ "\n",
+ " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
+ " img\n",
+ " );\n",
+ " fig.updated_canvas_event();\n",
+ " fig.waiting = false;\n",
+ " return;\n",
+ " } else if (\n",
+ " typeof evt.data === 'string' &&\n",
+ " evt.data.slice(0, 21) === 'data:image/png;base64'\n",
+ " ) {\n",
+ " fig.imageObj.src = evt.data;\n",
+ " fig.updated_canvas_event();\n",
+ " fig.waiting = false;\n",
+ " return;\n",
+ " }\n",
+ "\n",
+ " var msg = JSON.parse(evt.data);\n",
+ " var msg_type = msg['type'];\n",
+ "\n",
+ " // Call the \"handle_{type}\" callback, which takes\n",
+ " // the figure and JSON message as its only arguments.\n",
+ " try {\n",
+ " var callback = fig['handle_' + msg_type];\n",
+ " } catch (e) {\n",
+ " console.log(\n",
+ " \"No handler for the '\" + msg_type + \"' message type: \",\n",
+ " msg\n",
+ " );\n",
+ " return;\n",
+ " }\n",
+ "\n",
+ " if (callback) {\n",
+ " try {\n",
+ " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n",
+ " callback(fig, msg);\n",
+ " } catch (e) {\n",
+ " console.log(\n",
+ " \"Exception inside the 'handler_\" + msg_type + \"' callback:\",\n",
+ " e,\n",
+ " e.stack,\n",
+ " msg\n",
+ " );\n",
+ " }\n",
+ " }\n",
+ " };\n",
+ "};\n",
+ "\n",
+ "function getModifiers(event) {\n",
+ " var mods = [];\n",
+ " if (event.ctrlKey) {\n",
+ " mods.push('ctrl');\n",
+ " }\n",
+ " if (event.altKey) {\n",
+ " mods.push('alt');\n",
+ " }\n",
+ " if (event.shiftKey) {\n",
+ " mods.push('shift');\n",
+ " }\n",
+ " if (event.metaKey) {\n",
+ " mods.push('meta');\n",
+ " }\n",
+ " return mods;\n",
+ "}\n",
+ "\n",
+ "/*\n",
+ " * return a copy of an object with only non-object keys\n",
+ " * we need this to avoid circular references\n",
+ " * https://stackoverflow.com/a/24161582/3208463\n",
+ " */\n",
+ "function simpleKeys(original) {\n",
+ " return Object.keys(original).reduce(function (obj, key) {\n",
+ " if (typeof original[key] !== 'object') {\n",
+ " obj[key] = original[key];\n",
+ " }\n",
+ " return obj;\n",
+ " }, {});\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.mouse_event = function (event, name) {\n",
+ " if (name === 'button_press') {\n",
+ " this.canvas.focus();\n",
+ " this.canvas_div.focus();\n",
+ " }\n",
+ "\n",
+ " // from https://stackoverflow.com/q/1114465\n",
+ " var boundingRect = this.canvas.getBoundingClientRect();\n",
+ " var x = (event.clientX - boundingRect.left) * this.ratio;\n",
+ " var y = (event.clientY - boundingRect.top) * this.ratio;\n",
+ "\n",
+ " this.send_message(name, {\n",
+ " x: x,\n",
+ " y: y,\n",
+ " button: event.button,\n",
+ " step: event.step,\n",
+ " modifiers: getModifiers(event),\n",
+ " guiEvent: simpleKeys(event),\n",
+ " });\n",
+ "\n",
+ " return false;\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._key_event_extra = function (_event, _name) {\n",
+ " // Handle any extra behaviour associated with a key event\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.key_event = function (event, name) {\n",
+ " // Prevent repeat events\n",
+ " if (name === 'key_press') {\n",
+ " if (event.key === this._key) {\n",
+ " return;\n",
+ " } else {\n",
+ " this._key = event.key;\n",
+ " }\n",
+ " }\n",
+ " if (name === 'key_release') {\n",
+ " this._key = null;\n",
+ " }\n",
+ "\n",
+ " var value = '';\n",
+ " if (event.ctrlKey && event.key !== 'Control') {\n",
+ " value += 'ctrl+';\n",
+ " }\n",
+ " else if (event.altKey && event.key !== 'Alt') {\n",
+ " value += 'alt+';\n",
+ " }\n",
+ " else if (event.shiftKey && event.key !== 'Shift') {\n",
+ " value += 'shift+';\n",
+ " }\n",
+ "\n",
+ " value += 'k' + event.key;\n",
+ "\n",
+ " this._key_event_extra(event, name);\n",
+ "\n",
+ " this.send_message(name, { key: value, guiEvent: simpleKeys(event) });\n",
+ " return false;\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.toolbar_button_onclick = function (name) {\n",
+ " if (name === 'download') {\n",
+ " this.handle_save(this, null);\n",
+ " } else {\n",
+ " this.send_message('toolbar_button', { name: name });\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.toolbar_button_onmouseover = function (tooltip) {\n",
+ " this.message.textContent = tooltip;\n",
+ "};\n",
+ "\n",
+ "///////////////// REMAINING CONTENT GENERATED BY embed_js.py /////////////////\n",
+ "// prettier-ignore\n",
+ "var _JSXTOOLS_RESIZE_OBSERVER=function(A){var t,i=new WeakMap,n=new WeakMap,a=new WeakMap,r=new WeakMap,o=new Set;function s(e){if(!(this instanceof s))throw new TypeError(\"Constructor requires 'new' operator\");i.set(this,e)}function h(){throw new TypeError(\"Function is not a constructor\")}function c(e,t,i,n){e=0 in arguments?Number(arguments[0]):0,t=1 in arguments?Number(arguments[1]):0,i=2 in arguments?Number(arguments[2]):0,n=3 in arguments?Number(arguments[3]):0,this.right=(this.x=this.left=e)+(this.width=i),this.bottom=(this.y=this.top=t)+(this.height=n),Object.freeze(this)}function d(){t=requestAnimationFrame(d);var s=new WeakMap,p=new Set;o.forEach((function(t){r.get(t).forEach((function(i){var r=t instanceof window.SVGElement,o=a.get(t),d=r?0:parseFloat(o.paddingTop),f=r?0:parseFloat(o.paddingRight),l=r?0:parseFloat(o.paddingBottom),u=r?0:parseFloat(o.paddingLeft),g=r?0:parseFloat(o.borderTopWidth),m=r?0:parseFloat(o.borderRightWidth),w=r?0:parseFloat(o.borderBottomWidth),b=u+f,F=d+l,v=(r?0:parseFloat(o.borderLeftWidth))+m,W=g+w,y=r?0:t.offsetHeight-W-t.clientHeight,E=r?0:t.offsetWidth-v-t.clientWidth,R=b+v,z=F+W,M=r?t.width:parseFloat(o.width)-R-E,O=r?t.height:parseFloat(o.height)-z-y;if(n.has(t)){var k=n.get(t);if(k[0]===M&&k[1]===O)return}n.set(t,[M,O]);var S=Object.create(h.prototype);S.target=t,S.contentRect=new c(u,d,M,O),s.has(i)||(s.set(i,[]),p.add(i)),s.get(i).push(S)}))})),p.forEach((function(e){i.get(e).call(e,s.get(e),e)}))}return s.prototype.observe=function(i){if(i instanceof window.Element){r.has(i)||(r.set(i,new Set),o.add(i),a.set(i,window.getComputedStyle(i)));var n=r.get(i);n.has(this)||n.add(this),cancelAnimationFrame(t),t=requestAnimationFrame(d)}},s.prototype.unobserve=function(i){if(i instanceof window.Element&&r.has(i)){var n=r.get(i);n.has(this)&&(n.delete(this),n.size||(r.delete(i),o.delete(i))),n.size||r.delete(i),o.size||cancelAnimationFrame(t)}},A.DOMRectReadOnly=c,A.ResizeObserver=s,A.ResizeObserverEntry=h,A}; // eslint-disable-line\n",
+ "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Left button pans, Right button zooms\\nx/y fixes axis, CTRL fixes aspect\", \"fa fa-arrows\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\\nx/y fixes axis\", \"fa fa-square-o\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o\", \"download\"]];\n",
+ "\n",
+ "mpl.extensions = [\"eps\", \"jpeg\", \"pgf\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\", \"tif\", \"webp\"];\n",
+ "\n",
+ "mpl.default_extension = \"png\";/* global mpl */\n",
+ "\n",
+ "var comm_websocket_adapter = function (comm) {\n",
+ " // Create a \"websocket\"-like object which calls the given IPython comm\n",
+ " // object with the appropriate methods. Currently this is a non binary\n",
+ " // socket, so there is still some room for performance tuning.\n",
+ " var ws = {};\n",
+ "\n",
+ " ws.binaryType = comm.kernel.ws.binaryType;\n",
+ " ws.readyState = comm.kernel.ws.readyState;\n",
+ " function updateReadyState(_event) {\n",
+ " if (comm.kernel.ws) {\n",
+ " ws.readyState = comm.kernel.ws.readyState;\n",
+ " } else {\n",
+ " ws.readyState = 3; // Closed state.\n",
+ " }\n",
+ " }\n",
+ " comm.kernel.ws.addEventListener('open', updateReadyState);\n",
+ " comm.kernel.ws.addEventListener('close', updateReadyState);\n",
+ " comm.kernel.ws.addEventListener('error', updateReadyState);\n",
+ "\n",
+ " ws.close = function () {\n",
+ " comm.close();\n",
+ " };\n",
+ " ws.send = function (m) {\n",
+ " //console.log('sending', m);\n",
+ " comm.send(m);\n",
+ " };\n",
+ " // Register the callback with on_msg.\n",
+ " comm.on_msg(function (msg) {\n",
+ " //console.log('receiving', msg['content']['data'], msg);\n",
+ " var data = msg['content']['data'];\n",
+ " if (data['blob'] !== undefined) {\n",
+ " data = {\n",
+ " data: new Blob(msg['buffers'], { type: data['blob'] }),\n",
+ " };\n",
+ " }\n",
+ " // Pass the mpl event to the overridden (by mpl) onmessage function.\n",
+ " ws.onmessage(data);\n",
+ " });\n",
+ " return ws;\n",
+ "};\n",
+ "\n",
+ "mpl.mpl_figure_comm = function (comm, msg) {\n",
+ " // This is the function which gets called when the mpl process\n",
+ " // starts-up an IPython Comm through the \"matplotlib\" channel.\n",
+ "\n",
+ " var id = msg.content.data.id;\n",
+ " // Get hold of the div created by the display call when the Comm\n",
+ " // socket was opened in Python.\n",
+ " var element = document.getElementById(id);\n",
+ " var ws_proxy = comm_websocket_adapter(comm);\n",
+ "\n",
+ " function ondownload(figure, _format) {\n",
+ " window.open(figure.canvas.toDataURL());\n",
+ " }\n",
+ "\n",
+ " var fig = new mpl.figure(id, ws_proxy, ondownload, element);\n",
+ "\n",
+ " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n",
+ " // web socket which is closed, not our websocket->open comm proxy.\n",
+ " ws_proxy.onopen();\n",
+ "\n",
+ " fig.parent_element = element;\n",
+ " fig.cell_info = mpl.find_output_cell(\"\");\n",
+ " if (!fig.cell_info) {\n",
+ " console.error('Failed to find cell for figure', id, fig);\n",
+ " return;\n",
+ " }\n",
+ " fig.cell_info[0].output_area.element.on(\n",
+ " 'cleared',\n",
+ " { fig: fig },\n",
+ " fig._remove_fig_handler\n",
+ " );\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_close = function (fig, msg) {\n",
+ " var width = fig.canvas.width / fig.ratio;\n",
+ " fig.cell_info[0].output_area.element.off(\n",
+ " 'cleared',\n",
+ " fig._remove_fig_handler\n",
+ " );\n",
+ " fig.resizeObserverInstance.unobserve(fig.canvas_div);\n",
+ "\n",
+ " // Update the output cell to use the data from the current canvas.\n",
+ " fig.push_to_output();\n",
+ " var dataURL = fig.canvas.toDataURL();\n",
+ " // Re-enable the keyboard manager in IPython - without this line, in FF,\n",
+ " // the notebook keyboard shortcuts fail.\n",
+ " IPython.keyboard_manager.enable();\n",
+ " fig.parent_element.innerHTML =\n",
+ " '';\n",
+ " fig.close_ws(fig, msg);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.close_ws = function (fig, msg) {\n",
+ " fig.send_message('closing', msg);\n",
+ " // fig.ws.close()\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.push_to_output = function (_remove_interactive) {\n",
+ " // Turn the data on the canvas into data in the output cell.\n",
+ " var width = this.canvas.width / this.ratio;\n",
+ " var dataURL = this.canvas.toDataURL();\n",
+ " this.cell_info[1]['text/html'] =\n",
+ " '';\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.updated_canvas_event = function () {\n",
+ " // Tell IPython that the notebook contents must change.\n",
+ " IPython.notebook.set_dirty(true);\n",
+ " this.send_message('ack', {});\n",
+ " var fig = this;\n",
+ " // Wait a second, then push the new image to the DOM so\n",
+ " // that it is saved nicely (might be nice to debounce this).\n",
+ " setTimeout(function () {\n",
+ " fig.push_to_output();\n",
+ " }, 1000);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._init_toolbar = function () {\n",
+ " var fig = this;\n",
+ "\n",
+ " var toolbar = document.createElement('div');\n",
+ " toolbar.classList = 'btn-toolbar';\n",
+ " this.root.appendChild(toolbar);\n",
+ "\n",
+ " function on_click_closure(name) {\n",
+ " return function (_event) {\n",
+ " return fig.toolbar_button_onclick(name);\n",
+ " };\n",
+ " }\n",
+ "\n",
+ " function on_mouseover_closure(tooltip) {\n",
+ " return function (event) {\n",
+ " if (!event.currentTarget.disabled) {\n",
+ " return fig.toolbar_button_onmouseover(tooltip);\n",
+ " }\n",
+ " };\n",
+ " }\n",
+ "\n",
+ " fig.buttons = {};\n",
+ " var buttonGroup = document.createElement('div');\n",
+ " buttonGroup.classList = 'btn-group';\n",
+ " var button;\n",
+ " for (var toolbar_ind in mpl.toolbar_items) {\n",
+ " var name = mpl.toolbar_items[toolbar_ind][0];\n",
+ " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
+ " var image = mpl.toolbar_items[toolbar_ind][2];\n",
+ " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
+ "\n",
+ " if (!name) {\n",
+ " /* Instead of a spacer, we start a new button group. */\n",
+ " if (buttonGroup.hasChildNodes()) {\n",
+ " toolbar.appendChild(buttonGroup);\n",
+ " }\n",
+ " buttonGroup = document.createElement('div');\n",
+ " buttonGroup.classList = 'btn-group';\n",
+ " continue;\n",
+ " }\n",
+ "\n",
+ " button = fig.buttons[name] = document.createElement('button');\n",
+ " button.classList = 'btn btn-default';\n",
+ " button.href = '#';\n",
+ " button.title = name;\n",
+ " button.innerHTML = '';\n",
+ " button.addEventListener('click', on_click_closure(method_name));\n",
+ " button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n",
+ " buttonGroup.appendChild(button);\n",
+ " }\n",
+ "\n",
+ " if (buttonGroup.hasChildNodes()) {\n",
+ " toolbar.appendChild(buttonGroup);\n",
+ " }\n",
+ "\n",
+ " // Add the status bar.\n",
+ " var status_bar = document.createElement('span');\n",
+ " status_bar.classList = 'mpl-message pull-right';\n",
+ " toolbar.appendChild(status_bar);\n",
+ " this.message = status_bar;\n",
+ "\n",
+ " // Add the close button to the window.\n",
+ " var buttongrp = document.createElement('div');\n",
+ " buttongrp.classList = 'btn-group inline pull-right';\n",
+ " button = document.createElement('button');\n",
+ " button.classList = 'btn btn-mini btn-primary';\n",
+ " button.href = '#';\n",
+ " button.title = 'Stop Interaction';\n",
+ " button.innerHTML = '';\n",
+ " button.addEventListener('click', function (_evt) {\n",
+ " fig.handle_close(fig, {});\n",
+ " });\n",
+ " button.addEventListener(\n",
+ " 'mouseover',\n",
+ " on_mouseover_closure('Stop Interaction')\n",
+ " );\n",
+ " buttongrp.appendChild(button);\n",
+ " var titlebar = this.root.querySelector('.ui-dialog-titlebar');\n",
+ " titlebar.insertBefore(buttongrp, titlebar.firstChild);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._remove_fig_handler = function (event) {\n",
+ " var fig = event.data.fig;\n",
+ " if (event.target !== this) {\n",
+ " // Ignore bubbled events from children.\n",
+ " return;\n",
+ " }\n",
+ " fig.close_ws(fig, {});\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._root_extra_style = function (el) {\n",
+ " el.style.boxSizing = 'content-box'; // override notebook setting of border-box.\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._canvas_extra_style = function (el) {\n",
+ " // this is important to make the div 'focusable\n",
+ " el.setAttribute('tabindex', 0);\n",
+ " // reach out to IPython and tell the keyboard manager to turn it's self\n",
+ " // off when our div gets focus\n",
+ "\n",
+ " // location in version 3\n",
+ " if (IPython.notebook.keyboard_manager) {\n",
+ " IPython.notebook.keyboard_manager.register_events(el);\n",
+ " } else {\n",
+ " // location in version 2\n",
+ " IPython.keyboard_manager.register_events(el);\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._key_event_extra = function (event, _name) {\n",
+ " // Check for shift+enter\n",
+ " if (event.shiftKey && event.which === 13) {\n",
+ " this.canvas_div.blur();\n",
+ " // select the cell after this one\n",
+ " var index = IPython.notebook.find_cell_index(this.cell_info[0]);\n",
+ " IPython.notebook.select(index + 1);\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_save = function (fig, _msg) {\n",
+ " fig.ondownload(fig, null);\n",
+ "};\n",
+ "\n",
+ "mpl.find_output_cell = function (html_output) {\n",
+ " // Return the cell and output element which can be found *uniquely* in the notebook.\n",
+ " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n",
+ " // IPython event is triggered only after the cells have been serialised, which for\n",
+ " // our purposes (turning an active figure into a static one), is too late.\n",
+ " var cells = IPython.notebook.get_cells();\n",
+ " var ncells = cells.length;\n",
+ " for (var i = 0; i < ncells; i++) {\n",
+ " var cell = cells[i];\n",
+ " if (cell.cell_type === 'code') {\n",
+ " for (var j = 0; j < cell.output_area.outputs.length; j++) {\n",
+ " var data = cell.output_area.outputs[j];\n",
+ " if (data.data) {\n",
+ " // IPython >= 3 moved mimebundle to data attribute of output\n",
+ " data = data.data;\n",
+ " }\n",
+ " if (data['text/html'] === html_output) {\n",
+ " return [cell, data, j];\n",
+ " }\n",
+ " }\n",
+ " }\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "// Register the function which deals with the matplotlib target/channel.\n",
+ "// The kernel may be null if the page has been refreshed.\n",
+ "if (IPython.notebook.kernel !== null) {\n",
+ " IPython.notebook.kernel.comm_manager.register_target(\n",
+ " 'matplotlib',\n",
+ " mpl.mpl_figure_comm\n",
+ " );\n",
+ "}\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/html": [
+ ""
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/javascript": [
+ "/* Put everything inside the global mpl namespace */\n",
+ "/* global mpl */\n",
+ "window.mpl = {};\n",
+ "\n",
+ "mpl.get_websocket_type = function () {\n",
+ " if (typeof WebSocket !== 'undefined') {\n",
+ " return WebSocket;\n",
+ " } else if (typeof MozWebSocket !== 'undefined') {\n",
+ " return MozWebSocket;\n",
+ " } else {\n",
+ " alert(\n",
+ " 'Your browser does not have WebSocket support. ' +\n",
+ " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n",
+ " 'Firefox 4 and 5 are also supported but you ' +\n",
+ " 'have to enable WebSockets in about:config.'\n",
+ " );\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure = function (figure_id, websocket, ondownload, parent_element) {\n",
+ " this.id = figure_id;\n",
+ "\n",
+ " this.ws = websocket;\n",
+ "\n",
+ " this.supports_binary = this.ws.binaryType !== undefined;\n",
+ "\n",
+ " if (!this.supports_binary) {\n",
+ " var warnings = document.getElementById('mpl-warnings');\n",
+ " if (warnings) {\n",
+ " warnings.style.display = 'block';\n",
+ " warnings.textContent =\n",
+ " 'This browser does not support binary websocket messages. ' +\n",
+ " 'Performance may be slow.';\n",
+ " }\n",
+ " }\n",
+ "\n",
+ " this.imageObj = new Image();\n",
+ "\n",
+ " this.context = undefined;\n",
+ " this.message = undefined;\n",
+ " this.canvas = undefined;\n",
+ " this.rubberband_canvas = undefined;\n",
+ " this.rubberband_context = undefined;\n",
+ " this.format_dropdown = undefined;\n",
+ "\n",
+ " this.image_mode = 'full';\n",
+ "\n",
+ " this.root = document.createElement('div');\n",
+ " this.root.setAttribute('style', 'display: inline-block');\n",
+ " this._root_extra_style(this.root);\n",
+ "\n",
+ " parent_element.appendChild(this.root);\n",
+ "\n",
+ " this._init_header(this);\n",
+ " this._init_canvas(this);\n",
+ " this._init_toolbar(this);\n",
+ "\n",
+ " var fig = this;\n",
+ "\n",
+ " this.waiting = false;\n",
+ "\n",
+ " this.ws.onopen = function () {\n",
+ " fig.send_message('supports_binary', { value: fig.supports_binary });\n",
+ " fig.send_message('send_image_mode', {});\n",
+ " if (fig.ratio !== 1) {\n",
+ " fig.send_message('set_device_pixel_ratio', {\n",
+ " device_pixel_ratio: fig.ratio,\n",
+ " });\n",
+ " }\n",
+ " fig.send_message('refresh', {});\n",
+ " };\n",
+ "\n",
+ " this.imageObj.onload = function () {\n",
+ " if (fig.image_mode === 'full') {\n",
+ " // Full images could contain transparency (where diff images\n",
+ " // almost always do), so we need to clear the canvas so that\n",
+ " // there is no ghosting.\n",
+ " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n",
+ " }\n",
+ " fig.context.drawImage(fig.imageObj, 0, 0);\n",
+ " };\n",
+ "\n",
+ " this.imageObj.onunload = function () {\n",
+ " fig.ws.close();\n",
+ " };\n",
+ "\n",
+ " this.ws.onmessage = this._make_on_message_function(this);\n",
+ "\n",
+ " this.ondownload = ondownload;\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._init_header = function () {\n",
+ " var titlebar = document.createElement('div');\n",
+ " titlebar.classList =\n",
+ " 'ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix';\n",
+ " var titletext = document.createElement('div');\n",
+ " titletext.classList = 'ui-dialog-title';\n",
+ " titletext.setAttribute(\n",
+ " 'style',\n",
+ " 'width: 100%; text-align: center; padding: 3px;'\n",
+ " );\n",
+ " titlebar.appendChild(titletext);\n",
+ " this.root.appendChild(titlebar);\n",
+ " this.header = titletext;\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._canvas_extra_style = function (_canvas_div) {};\n",
+ "\n",
+ "mpl.figure.prototype._root_extra_style = function (_canvas_div) {};\n",
+ "\n",
+ "mpl.figure.prototype._init_canvas = function () {\n",
+ " var fig = this;\n",
+ "\n",
+ " var canvas_div = (this.canvas_div = document.createElement('div'));\n",
+ " canvas_div.setAttribute('tabindex', '0');\n",
+ " canvas_div.setAttribute(\n",
+ " 'style',\n",
+ " 'border: 1px solid #ddd;' +\n",
+ " 'box-sizing: content-box;' +\n",
+ " 'clear: both;' +\n",
+ " 'min-height: 1px;' +\n",
+ " 'min-width: 1px;' +\n",
+ " 'outline: 0;' +\n",
+ " 'overflow: hidden;' +\n",
+ " 'position: relative;' +\n",
+ " 'resize: both;' +\n",
+ " 'z-index: 2;'\n",
+ " );\n",
+ "\n",
+ " function on_keyboard_event_closure(name) {\n",
+ " return function (event) {\n",
+ " return fig.key_event(event, name);\n",
+ " };\n",
+ " }\n",
+ "\n",
+ " canvas_div.addEventListener(\n",
+ " 'keydown',\n",
+ " on_keyboard_event_closure('key_press')\n",
+ " );\n",
+ " canvas_div.addEventListener(\n",
+ " 'keyup',\n",
+ " on_keyboard_event_closure('key_release')\n",
+ " );\n",
+ "\n",
+ " this._canvas_extra_style(canvas_div);\n",
+ " this.root.appendChild(canvas_div);\n",
+ "\n",
+ " var canvas = (this.canvas = document.createElement('canvas'));\n",
+ " canvas.classList.add('mpl-canvas');\n",
+ " canvas.setAttribute(\n",
+ " 'style',\n",
+ " 'box-sizing: content-box;' +\n",
+ " 'pointer-events: none;' +\n",
+ " 'position: relative;' +\n",
+ " 'z-index: 0;'\n",
+ " );\n",
+ "\n",
+ " this.context = canvas.getContext('2d');\n",
+ "\n",
+ " var backingStore =\n",
+ " this.context.backingStorePixelRatio ||\n",
+ " this.context.webkitBackingStorePixelRatio ||\n",
+ " this.context.mozBackingStorePixelRatio ||\n",
+ " this.context.msBackingStorePixelRatio ||\n",
+ " this.context.oBackingStorePixelRatio ||\n",
+ " this.context.backingStorePixelRatio ||\n",
+ " 1;\n",
+ "\n",
+ " this.ratio = (window.devicePixelRatio || 1) / backingStore;\n",
+ "\n",
+ " var rubberband_canvas = (this.rubberband_canvas = document.createElement(\n",
+ " 'canvas'\n",
+ " ));\n",
+ " rubberband_canvas.setAttribute(\n",
+ " 'style',\n",
+ " 'box-sizing: content-box;' +\n",
+ " 'left: 0;' +\n",
+ " 'pointer-events: none;' +\n",
+ " 'position: absolute;' +\n",
+ " 'top: 0;' +\n",
+ " 'z-index: 1;'\n",
+ " );\n",
+ "\n",
+ " // Apply a ponyfill if ResizeObserver is not implemented by browser.\n",
+ " if (this.ResizeObserver === undefined) {\n",
+ " if (window.ResizeObserver !== undefined) {\n",
+ " this.ResizeObserver = window.ResizeObserver;\n",
+ " } else {\n",
+ " var obs = _JSXTOOLS_RESIZE_OBSERVER({});\n",
+ " this.ResizeObserver = obs.ResizeObserver;\n",
+ " }\n",
+ " }\n",
+ "\n",
+ " this.resizeObserverInstance = new this.ResizeObserver(function (entries) {\n",
+ " var nentries = entries.length;\n",
+ " for (var i = 0; i < nentries; i++) {\n",
+ " var entry = entries[i];\n",
+ " var width, height;\n",
+ " if (entry.contentBoxSize) {\n",
+ " if (entry.contentBoxSize instanceof Array) {\n",
+ " // Chrome 84 implements new version of spec.\n",
+ " width = entry.contentBoxSize[0].inlineSize;\n",
+ " height = entry.contentBoxSize[0].blockSize;\n",
+ " } else {\n",
+ " // Firefox implements old version of spec.\n",
+ " width = entry.contentBoxSize.inlineSize;\n",
+ " height = entry.contentBoxSize.blockSize;\n",
+ " }\n",
+ " } else {\n",
+ " // Chrome <84 implements even older version of spec.\n",
+ " width = entry.contentRect.width;\n",
+ " height = entry.contentRect.height;\n",
+ " }\n",
+ "\n",
+ " // Keep the size of the canvas and rubber band canvas in sync with\n",
+ " // the canvas container.\n",
+ " if (entry.devicePixelContentBoxSize) {\n",
+ " // Chrome 84 implements new version of spec.\n",
+ " canvas.setAttribute(\n",
+ " 'width',\n",
+ " entry.devicePixelContentBoxSize[0].inlineSize\n",
+ " );\n",
+ " canvas.setAttribute(\n",
+ " 'height',\n",
+ " entry.devicePixelContentBoxSize[0].blockSize\n",
+ " );\n",
+ " } else {\n",
+ " canvas.setAttribute('width', width * fig.ratio);\n",
+ " canvas.setAttribute('height', height * fig.ratio);\n",
+ " }\n",
+ " /* This rescales the canvas back to display pixels, so that it\n",
+ " * appears correct on HiDPI screens. */\n",
+ " canvas.style.width = width + 'px';\n",
+ " canvas.style.height = height + 'px';\n",
+ "\n",
+ " rubberband_canvas.setAttribute('width', width);\n",
+ " rubberband_canvas.setAttribute('height', height);\n",
+ "\n",
+ " // And update the size in Python. We ignore the initial 0/0 size\n",
+ " // that occurs as the element is placed into the DOM, which should\n",
+ " // otherwise not happen due to the minimum size styling.\n",
+ " if (fig.ws.readyState == 1 && width != 0 && height != 0) {\n",
+ " fig.request_resize(width, height);\n",
+ " }\n",
+ " }\n",
+ " });\n",
+ " this.resizeObserverInstance.observe(canvas_div);\n",
+ "\n",
+ " function on_mouse_event_closure(name) {\n",
+ " /* User Agent sniffing is bad, but WebKit is busted:\n",
+ " * https://bugs.webkit.org/show_bug.cgi?id=144526\n",
+ " * https://bugs.webkit.org/show_bug.cgi?id=181818\n",
+ " * The worst that happens here is that they get an extra browser\n",
+ " * selection when dragging, if this check fails to catch them.\n",
+ " */\n",
+ " var UA = navigator.userAgent;\n",
+ " var isWebKit = /AppleWebKit/.test(UA) && !/Chrome/.test(UA);\n",
+ " if(isWebKit) {\n",
+ " return function (event) {\n",
+ " /* This prevents the web browser from automatically changing to\n",
+ " * the text insertion cursor when the button is pressed. We\n",
+ " * want to control all of the cursor setting manually through\n",
+ " * the 'cursor' event from matplotlib */\n",
+ " event.preventDefault()\n",
+ " return fig.mouse_event(event, name);\n",
+ " };\n",
+ " } else {\n",
+ " return function (event) {\n",
+ " return fig.mouse_event(event, name);\n",
+ " };\n",
+ " }\n",
+ " }\n",
+ "\n",
+ " canvas_div.addEventListener(\n",
+ " 'mousedown',\n",
+ " on_mouse_event_closure('button_press')\n",
+ " );\n",
+ " canvas_div.addEventListener(\n",
+ " 'mouseup',\n",
+ " on_mouse_event_closure('button_release')\n",
+ " );\n",
+ " canvas_div.addEventListener(\n",
+ " 'dblclick',\n",
+ " on_mouse_event_closure('dblclick')\n",
+ " );\n",
+ " // Throttle sequential mouse events to 1 every 20ms.\n",
+ " canvas_div.addEventListener(\n",
+ " 'mousemove',\n",
+ " on_mouse_event_closure('motion_notify')\n",
+ " );\n",
+ "\n",
+ " canvas_div.addEventListener(\n",
+ " 'mouseenter',\n",
+ " on_mouse_event_closure('figure_enter')\n",
+ " );\n",
+ " canvas_div.addEventListener(\n",
+ " 'mouseleave',\n",
+ " on_mouse_event_closure('figure_leave')\n",
+ " );\n",
+ "\n",
+ " canvas_div.addEventListener('wheel', function (event) {\n",
+ " if (event.deltaY < 0) {\n",
+ " event.step = 1;\n",
+ " } else {\n",
+ " event.step = -1;\n",
+ " }\n",
+ " on_mouse_event_closure('scroll')(event);\n",
+ " });\n",
+ "\n",
+ " canvas_div.appendChild(canvas);\n",
+ " canvas_div.appendChild(rubberband_canvas);\n",
+ "\n",
+ " this.rubberband_context = rubberband_canvas.getContext('2d');\n",
+ " this.rubberband_context.strokeStyle = '#000000';\n",
+ "\n",
+ " this._resize_canvas = function (width, height, forward) {\n",
+ " if (forward) {\n",
+ " canvas_div.style.width = width + 'px';\n",
+ " canvas_div.style.height = height + 'px';\n",
+ " }\n",
+ " };\n",
+ "\n",
+ " // Disable right mouse context menu.\n",
+ " canvas_div.addEventListener('contextmenu', function (_e) {\n",
+ " event.preventDefault();\n",
+ " return false;\n",
+ " });\n",
+ "\n",
+ " function set_focus() {\n",
+ " canvas.focus();\n",
+ " canvas_div.focus();\n",
+ " }\n",
+ "\n",
+ " window.setTimeout(set_focus, 100);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._init_toolbar = function () {\n",
+ " var fig = this;\n",
+ "\n",
+ " var toolbar = document.createElement('div');\n",
+ " toolbar.classList = 'mpl-toolbar';\n",
+ " this.root.appendChild(toolbar);\n",
+ "\n",
+ " function on_click_closure(name) {\n",
+ " return function (_event) {\n",
+ " return fig.toolbar_button_onclick(name);\n",
+ " };\n",
+ " }\n",
+ "\n",
+ " function on_mouseover_closure(tooltip) {\n",
+ " return function (event) {\n",
+ " if (!event.currentTarget.disabled) {\n",
+ " return fig.toolbar_button_onmouseover(tooltip);\n",
+ " }\n",
+ " };\n",
+ " }\n",
+ "\n",
+ " fig.buttons = {};\n",
+ " var buttonGroup = document.createElement('div');\n",
+ " buttonGroup.classList = 'mpl-button-group';\n",
+ " for (var toolbar_ind in mpl.toolbar_items) {\n",
+ " var name = mpl.toolbar_items[toolbar_ind][0];\n",
+ " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
+ " var image = mpl.toolbar_items[toolbar_ind][2];\n",
+ " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
+ "\n",
+ " if (!name) {\n",
+ " /* Instead of a spacer, we start a new button group. */\n",
+ " if (buttonGroup.hasChildNodes()) {\n",
+ " toolbar.appendChild(buttonGroup);\n",
+ " }\n",
+ " buttonGroup = document.createElement('div');\n",
+ " buttonGroup.classList = 'mpl-button-group';\n",
+ " continue;\n",
+ " }\n",
+ "\n",
+ " var button = (fig.buttons[name] = document.createElement('button'));\n",
+ " button.classList = 'mpl-widget';\n",
+ " button.setAttribute('role', 'button');\n",
+ " button.setAttribute('aria-disabled', 'false');\n",
+ " button.addEventListener('click', on_click_closure(method_name));\n",
+ " button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n",
+ "\n",
+ " var icon_img = document.createElement('img');\n",
+ " icon_img.src = '_images/' + image + '.png';\n",
+ " icon_img.srcset = '_images/' + image + '_large.png 2x';\n",
+ " icon_img.alt = tooltip;\n",
+ " button.appendChild(icon_img);\n",
+ "\n",
+ " buttonGroup.appendChild(button);\n",
+ " }\n",
+ "\n",
+ " if (buttonGroup.hasChildNodes()) {\n",
+ " toolbar.appendChild(buttonGroup);\n",
+ " }\n",
+ "\n",
+ " var fmt_picker = document.createElement('select');\n",
+ " fmt_picker.classList = 'mpl-widget';\n",
+ " toolbar.appendChild(fmt_picker);\n",
+ " this.format_dropdown = fmt_picker;\n",
+ "\n",
+ " for (var ind in mpl.extensions) {\n",
+ " var fmt = mpl.extensions[ind];\n",
+ " var option = document.createElement('option');\n",
+ " option.selected = fmt === mpl.default_extension;\n",
+ " option.innerHTML = fmt;\n",
+ " fmt_picker.appendChild(option);\n",
+ " }\n",
+ "\n",
+ " var status_bar = document.createElement('span');\n",
+ " status_bar.classList = 'mpl-message';\n",
+ " toolbar.appendChild(status_bar);\n",
+ " this.message = status_bar;\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.request_resize = function (x_pixels, y_pixels) {\n",
+ " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n",
+ " // which will in turn request a refresh of the image.\n",
+ " this.send_message('resize', { width: x_pixels, height: y_pixels });\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.send_message = function (type, properties) {\n",
+ " properties['type'] = type;\n",
+ " properties['figure_id'] = this.id;\n",
+ " this.ws.send(JSON.stringify(properties));\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.send_draw_message = function () {\n",
+ " if (!this.waiting) {\n",
+ " this.waiting = true;\n",
+ " this.ws.send(JSON.stringify({ type: 'draw', figure_id: this.id }));\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_save = function (fig, _msg) {\n",
+ " var format_dropdown = fig.format_dropdown;\n",
+ " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n",
+ " fig.ondownload(fig, format);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_resize = function (fig, msg) {\n",
+ " var size = msg['size'];\n",
+ " if (size[0] !== fig.canvas.width || size[1] !== fig.canvas.height) {\n",
+ " fig._resize_canvas(size[0], size[1], msg['forward']);\n",
+ " fig.send_message('refresh', {});\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_rubberband = function (fig, msg) {\n",
+ " var x0 = msg['x0'] / fig.ratio;\n",
+ " var y0 = (fig.canvas.height - msg['y0']) / fig.ratio;\n",
+ " var x1 = msg['x1'] / fig.ratio;\n",
+ " var y1 = (fig.canvas.height - msg['y1']) / fig.ratio;\n",
+ " x0 = Math.floor(x0) + 0.5;\n",
+ " y0 = Math.floor(y0) + 0.5;\n",
+ " x1 = Math.floor(x1) + 0.5;\n",
+ " y1 = Math.floor(y1) + 0.5;\n",
+ " var min_x = Math.min(x0, x1);\n",
+ " var min_y = Math.min(y0, y1);\n",
+ " var width = Math.abs(x1 - x0);\n",
+ " var height = Math.abs(y1 - y0);\n",
+ "\n",
+ " fig.rubberband_context.clearRect(\n",
+ " 0,\n",
+ " 0,\n",
+ " fig.canvas.width / fig.ratio,\n",
+ " fig.canvas.height / fig.ratio\n",
+ " );\n",
+ "\n",
+ " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_figure_label = function (fig, msg) {\n",
+ " // Updates the figure title.\n",
+ " fig.header.textContent = msg['label'];\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_cursor = function (fig, msg) {\n",
+ " fig.canvas_div.style.cursor = msg['cursor'];\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_message = function (fig, msg) {\n",
+ " fig.message.textContent = msg['message'];\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_draw = function (fig, _msg) {\n",
+ " // Request the server to send over a new figure.\n",
+ " fig.send_draw_message();\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_image_mode = function (fig, msg) {\n",
+ " fig.image_mode = msg['mode'];\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_history_buttons = function (fig, msg) {\n",
+ " for (var key in msg) {\n",
+ " if (!(key in fig.buttons)) {\n",
+ " continue;\n",
+ " }\n",
+ " fig.buttons[key].disabled = !msg[key];\n",
+ " fig.buttons[key].setAttribute('aria-disabled', !msg[key]);\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_navigate_mode = function (fig, msg) {\n",
+ " if (msg['mode'] === 'PAN') {\n",
+ " fig.buttons['Pan'].classList.add('active');\n",
+ " fig.buttons['Zoom'].classList.remove('active');\n",
+ " } else if (msg['mode'] === 'ZOOM') {\n",
+ " fig.buttons['Pan'].classList.remove('active');\n",
+ " fig.buttons['Zoom'].classList.add('active');\n",
+ " } else {\n",
+ " fig.buttons['Pan'].classList.remove('active');\n",
+ " fig.buttons['Zoom'].classList.remove('active');\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.updated_canvas_event = function () {\n",
+ " // Called whenever the canvas gets updated.\n",
+ " this.send_message('ack', {});\n",
+ "};\n",
+ "\n",
+ "// A function to construct a web socket function for onmessage handling.\n",
+ "// Called in the figure constructor.\n",
+ "mpl.figure.prototype._make_on_message_function = function (fig) {\n",
+ " return function socket_on_message(evt) {\n",
+ " if (evt.data instanceof Blob) {\n",
+ " var img = evt.data;\n",
+ " if (img.type !== 'image/png') {\n",
+ " /* FIXME: We get \"Resource interpreted as Image but\n",
+ " * transferred with MIME type text/plain:\" errors on\n",
+ " * Chrome. But how to set the MIME type? It doesn't seem\n",
+ " * to be part of the websocket stream */\n",
+ " img.type = 'image/png';\n",
+ " }\n",
+ "\n",
+ " /* Free the memory for the previous frames */\n",
+ " if (fig.imageObj.src) {\n",
+ " (window.URL || window.webkitURL).revokeObjectURL(\n",
+ " fig.imageObj.src\n",
+ " );\n",
+ " }\n",
+ "\n",
+ " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
+ " img\n",
+ " );\n",
+ " fig.updated_canvas_event();\n",
+ " fig.waiting = false;\n",
+ " return;\n",
+ " } else if (\n",
+ " typeof evt.data === 'string' &&\n",
+ " evt.data.slice(0, 21) === 'data:image/png;base64'\n",
+ " ) {\n",
+ " fig.imageObj.src = evt.data;\n",
+ " fig.updated_canvas_event();\n",
+ " fig.waiting = false;\n",
+ " return;\n",
+ " }\n",
+ "\n",
+ " var msg = JSON.parse(evt.data);\n",
+ " var msg_type = msg['type'];\n",
+ "\n",
+ " // Call the \"handle_{type}\" callback, which takes\n",
+ " // the figure and JSON message as its only arguments.\n",
+ " try {\n",
+ " var callback = fig['handle_' + msg_type];\n",
+ " } catch (e) {\n",
+ " console.log(\n",
+ " \"No handler for the '\" + msg_type + \"' message type: \",\n",
+ " msg\n",
+ " );\n",
+ " return;\n",
+ " }\n",
+ "\n",
+ " if (callback) {\n",
+ " try {\n",
+ " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n",
+ " callback(fig, msg);\n",
+ " } catch (e) {\n",
+ " console.log(\n",
+ " \"Exception inside the 'handler_\" + msg_type + \"' callback:\",\n",
+ " e,\n",
+ " e.stack,\n",
+ " msg\n",
+ " );\n",
+ " }\n",
+ " }\n",
+ " };\n",
+ "};\n",
+ "\n",
+ "function getModifiers(event) {\n",
+ " var mods = [];\n",
+ " if (event.ctrlKey) {\n",
+ " mods.push('ctrl');\n",
+ " }\n",
+ " if (event.altKey) {\n",
+ " mods.push('alt');\n",
+ " }\n",
+ " if (event.shiftKey) {\n",
+ " mods.push('shift');\n",
+ " }\n",
+ " if (event.metaKey) {\n",
+ " mods.push('meta');\n",
+ " }\n",
+ " return mods;\n",
+ "}\n",
+ "\n",
+ "/*\n",
+ " * return a copy of an object with only non-object keys\n",
+ " * we need this to avoid circular references\n",
+ " * https://stackoverflow.com/a/24161582/3208463\n",
+ " */\n",
+ "function simpleKeys(original) {\n",
+ " return Object.keys(original).reduce(function (obj, key) {\n",
+ " if (typeof original[key] !== 'object') {\n",
+ " obj[key] = original[key];\n",
+ " }\n",
+ " return obj;\n",
+ " }, {});\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.mouse_event = function (event, name) {\n",
+ " if (name === 'button_press') {\n",
+ " this.canvas.focus();\n",
+ " this.canvas_div.focus();\n",
+ " }\n",
+ "\n",
+ " // from https://stackoverflow.com/q/1114465\n",
+ " var boundingRect = this.canvas.getBoundingClientRect();\n",
+ " var x = (event.clientX - boundingRect.left) * this.ratio;\n",
+ " var y = (event.clientY - boundingRect.top) * this.ratio;\n",
+ "\n",
+ " this.send_message(name, {\n",
+ " x: x,\n",
+ " y: y,\n",
+ " button: event.button,\n",
+ " step: event.step,\n",
+ " modifiers: getModifiers(event),\n",
+ " guiEvent: simpleKeys(event),\n",
+ " });\n",
+ "\n",
+ " return false;\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._key_event_extra = function (_event, _name) {\n",
+ " // Handle any extra behaviour associated with a key event\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.key_event = function (event, name) {\n",
+ " // Prevent repeat events\n",
+ " if (name === 'key_press') {\n",
+ " if (event.key === this._key) {\n",
+ " return;\n",
+ " } else {\n",
+ " this._key = event.key;\n",
+ " }\n",
+ " }\n",
+ " if (name === 'key_release') {\n",
+ " this._key = null;\n",
+ " }\n",
+ "\n",
+ " var value = '';\n",
+ " if (event.ctrlKey && event.key !== 'Control') {\n",
+ " value += 'ctrl+';\n",
+ " }\n",
+ " else if (event.altKey && event.key !== 'Alt') {\n",
+ " value += 'alt+';\n",
+ " }\n",
+ " else if (event.shiftKey && event.key !== 'Shift') {\n",
+ " value += 'shift+';\n",
+ " }\n",
+ "\n",
+ " value += 'k' + event.key;\n",
+ "\n",
+ " this._key_event_extra(event, name);\n",
+ "\n",
+ " this.send_message(name, { key: value, guiEvent: simpleKeys(event) });\n",
+ " return false;\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.toolbar_button_onclick = function (name) {\n",
+ " if (name === 'download') {\n",
+ " this.handle_save(this, null);\n",
+ " } else {\n",
+ " this.send_message('toolbar_button', { name: name });\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.toolbar_button_onmouseover = function (tooltip) {\n",
+ " this.message.textContent = tooltip;\n",
+ "};\n",
+ "\n",
+ "///////////////// REMAINING CONTENT GENERATED BY embed_js.py /////////////////\n",
+ "// prettier-ignore\n",
+ "var _JSXTOOLS_RESIZE_OBSERVER=function(A){var t,i=new WeakMap,n=new WeakMap,a=new WeakMap,r=new WeakMap,o=new Set;function s(e){if(!(this instanceof s))throw new TypeError(\"Constructor requires 'new' operator\");i.set(this,e)}function h(){throw new TypeError(\"Function is not a constructor\")}function c(e,t,i,n){e=0 in arguments?Number(arguments[0]):0,t=1 in arguments?Number(arguments[1]):0,i=2 in arguments?Number(arguments[2]):0,n=3 in arguments?Number(arguments[3]):0,this.right=(this.x=this.left=e)+(this.width=i),this.bottom=(this.y=this.top=t)+(this.height=n),Object.freeze(this)}function d(){t=requestAnimationFrame(d);var s=new WeakMap,p=new Set;o.forEach((function(t){r.get(t).forEach((function(i){var r=t instanceof window.SVGElement,o=a.get(t),d=r?0:parseFloat(o.paddingTop),f=r?0:parseFloat(o.paddingRight),l=r?0:parseFloat(o.paddingBottom),u=r?0:parseFloat(o.paddingLeft),g=r?0:parseFloat(o.borderTopWidth),m=r?0:parseFloat(o.borderRightWidth),w=r?0:parseFloat(o.borderBottomWidth),b=u+f,F=d+l,v=(r?0:parseFloat(o.borderLeftWidth))+m,W=g+w,y=r?0:t.offsetHeight-W-t.clientHeight,E=r?0:t.offsetWidth-v-t.clientWidth,R=b+v,z=F+W,M=r?t.width:parseFloat(o.width)-R-E,O=r?t.height:parseFloat(o.height)-z-y;if(n.has(t)){var k=n.get(t);if(k[0]===M&&k[1]===O)return}n.set(t,[M,O]);var S=Object.create(h.prototype);S.target=t,S.contentRect=new c(u,d,M,O),s.has(i)||(s.set(i,[]),p.add(i)),s.get(i).push(S)}))})),p.forEach((function(e){i.get(e).call(e,s.get(e),e)}))}return s.prototype.observe=function(i){if(i instanceof window.Element){r.has(i)||(r.set(i,new Set),o.add(i),a.set(i,window.getComputedStyle(i)));var n=r.get(i);n.has(this)||n.add(this),cancelAnimationFrame(t),t=requestAnimationFrame(d)}},s.prototype.unobserve=function(i){if(i instanceof window.Element&&r.has(i)){var n=r.get(i);n.has(this)&&(n.delete(this),n.size||(r.delete(i),o.delete(i))),n.size||r.delete(i),o.size||cancelAnimationFrame(t)}},A.DOMRectReadOnly=c,A.ResizeObserver=s,A.ResizeObserverEntry=h,A}; // eslint-disable-line\n",
+ "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Left button pans, Right button zooms\\nx/y fixes axis, CTRL fixes aspect\", \"fa fa-arrows\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\\nx/y fixes axis\", \"fa fa-square-o\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o\", \"download\"]];\n",
+ "\n",
+ "mpl.extensions = [\"eps\", \"jpeg\", \"pgf\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\", \"tif\", \"webp\"];\n",
+ "\n",
+ "mpl.default_extension = \"png\";/* global mpl */\n",
+ "\n",
+ "var comm_websocket_adapter = function (comm) {\n",
+ " // Create a \"websocket\"-like object which calls the given IPython comm\n",
+ " // object with the appropriate methods. Currently this is a non binary\n",
+ " // socket, so there is still some room for performance tuning.\n",
+ " var ws = {};\n",
+ "\n",
+ " ws.binaryType = comm.kernel.ws.binaryType;\n",
+ " ws.readyState = comm.kernel.ws.readyState;\n",
+ " function updateReadyState(_event) {\n",
+ " if (comm.kernel.ws) {\n",
+ " ws.readyState = comm.kernel.ws.readyState;\n",
+ " } else {\n",
+ " ws.readyState = 3; // Closed state.\n",
+ " }\n",
+ " }\n",
+ " comm.kernel.ws.addEventListener('open', updateReadyState);\n",
+ " comm.kernel.ws.addEventListener('close', updateReadyState);\n",
+ " comm.kernel.ws.addEventListener('error', updateReadyState);\n",
+ "\n",
+ " ws.close = function () {\n",
+ " comm.close();\n",
+ " };\n",
+ " ws.send = function (m) {\n",
+ " //console.log('sending', m);\n",
+ " comm.send(m);\n",
+ " };\n",
+ " // Register the callback with on_msg.\n",
+ " comm.on_msg(function (msg) {\n",
+ " //console.log('receiving', msg['content']['data'], msg);\n",
+ " var data = msg['content']['data'];\n",
+ " if (data['blob'] !== undefined) {\n",
+ " data = {\n",
+ " data: new Blob(msg['buffers'], { type: data['blob'] }),\n",
+ " };\n",
+ " }\n",
+ " // Pass the mpl event to the overridden (by mpl) onmessage function.\n",
+ " ws.onmessage(data);\n",
+ " });\n",
+ " return ws;\n",
+ "};\n",
+ "\n",
+ "mpl.mpl_figure_comm = function (comm, msg) {\n",
+ " // This is the function which gets called when the mpl process\n",
+ " // starts-up an IPython Comm through the \"matplotlib\" channel.\n",
+ "\n",
+ " var id = msg.content.data.id;\n",
+ " // Get hold of the div created by the display call when the Comm\n",
+ " // socket was opened in Python.\n",
+ " var element = document.getElementById(id);\n",
+ " var ws_proxy = comm_websocket_adapter(comm);\n",
+ "\n",
+ " function ondownload(figure, _format) {\n",
+ " window.open(figure.canvas.toDataURL());\n",
+ " }\n",
+ "\n",
+ " var fig = new mpl.figure(id, ws_proxy, ondownload, element);\n",
+ "\n",
+ " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n",
+ " // web socket which is closed, not our websocket->open comm proxy.\n",
+ " ws_proxy.onopen();\n",
+ "\n",
+ " fig.parent_element = element;\n",
+ " fig.cell_info = mpl.find_output_cell(\"\");\n",
+ " if (!fig.cell_info) {\n",
+ " console.error('Failed to find cell for figure', id, fig);\n",
+ " return;\n",
+ " }\n",
+ " fig.cell_info[0].output_area.element.on(\n",
+ " 'cleared',\n",
+ " { fig: fig },\n",
+ " fig._remove_fig_handler\n",
+ " );\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_close = function (fig, msg) {\n",
+ " var width = fig.canvas.width / fig.ratio;\n",
+ " fig.cell_info[0].output_area.element.off(\n",
+ " 'cleared',\n",
+ " fig._remove_fig_handler\n",
+ " );\n",
+ " fig.resizeObserverInstance.unobserve(fig.canvas_div);\n",
+ "\n",
+ " // Update the output cell to use the data from the current canvas.\n",
+ " fig.push_to_output();\n",
+ " var dataURL = fig.canvas.toDataURL();\n",
+ " // Re-enable the keyboard manager in IPython - without this line, in FF,\n",
+ " // the notebook keyboard shortcuts fail.\n",
+ " IPython.keyboard_manager.enable();\n",
+ " fig.parent_element.innerHTML =\n",
+ " '';\n",
+ " fig.close_ws(fig, msg);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.close_ws = function (fig, msg) {\n",
+ " fig.send_message('closing', msg);\n",
+ " // fig.ws.close()\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.push_to_output = function (_remove_interactive) {\n",
+ " // Turn the data on the canvas into data in the output cell.\n",
+ " var width = this.canvas.width / this.ratio;\n",
+ " var dataURL = this.canvas.toDataURL();\n",
+ " this.cell_info[1]['text/html'] =\n",
+ " '';\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.updated_canvas_event = function () {\n",
+ " // Tell IPython that the notebook contents must change.\n",
+ " IPython.notebook.set_dirty(true);\n",
+ " this.send_message('ack', {});\n",
+ " var fig = this;\n",
+ " // Wait a second, then push the new image to the DOM so\n",
+ " // that it is saved nicely (might be nice to debounce this).\n",
+ " setTimeout(function () {\n",
+ " fig.push_to_output();\n",
+ " }, 1000);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._init_toolbar = function () {\n",
+ " var fig = this;\n",
+ "\n",
+ " var toolbar = document.createElement('div');\n",
+ " toolbar.classList = 'btn-toolbar';\n",
+ " this.root.appendChild(toolbar);\n",
+ "\n",
+ " function on_click_closure(name) {\n",
+ " return function (_event) {\n",
+ " return fig.toolbar_button_onclick(name);\n",
+ " };\n",
+ " }\n",
+ "\n",
+ " function on_mouseover_closure(tooltip) {\n",
+ " return function (event) {\n",
+ " if (!event.currentTarget.disabled) {\n",
+ " return fig.toolbar_button_onmouseover(tooltip);\n",
+ " }\n",
+ " };\n",
+ " }\n",
+ "\n",
+ " fig.buttons = {};\n",
+ " var buttonGroup = document.createElement('div');\n",
+ " buttonGroup.classList = 'btn-group';\n",
+ " var button;\n",
+ " for (var toolbar_ind in mpl.toolbar_items) {\n",
+ " var name = mpl.toolbar_items[toolbar_ind][0];\n",
+ " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
+ " var image = mpl.toolbar_items[toolbar_ind][2];\n",
+ " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
+ "\n",
+ " if (!name) {\n",
+ " /* Instead of a spacer, we start a new button group. */\n",
+ " if (buttonGroup.hasChildNodes()) {\n",
+ " toolbar.appendChild(buttonGroup);\n",
+ " }\n",
+ " buttonGroup = document.createElement('div');\n",
+ " buttonGroup.classList = 'btn-group';\n",
+ " continue;\n",
+ " }\n",
+ "\n",
+ " button = fig.buttons[name] = document.createElement('button');\n",
+ " button.classList = 'btn btn-default';\n",
+ " button.href = '#';\n",
+ " button.title = name;\n",
+ " button.innerHTML = '';\n",
+ " button.addEventListener('click', on_click_closure(method_name));\n",
+ " button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n",
+ " buttonGroup.appendChild(button);\n",
+ " }\n",
+ "\n",
+ " if (buttonGroup.hasChildNodes()) {\n",
+ " toolbar.appendChild(buttonGroup);\n",
+ " }\n",
+ "\n",
+ " // Add the status bar.\n",
+ " var status_bar = document.createElement('span');\n",
+ " status_bar.classList = 'mpl-message pull-right';\n",
+ " toolbar.appendChild(status_bar);\n",
+ " this.message = status_bar;\n",
+ "\n",
+ " // Add the close button to the window.\n",
+ " var buttongrp = document.createElement('div');\n",
+ " buttongrp.classList = 'btn-group inline pull-right';\n",
+ " button = document.createElement('button');\n",
+ " button.classList = 'btn btn-mini btn-primary';\n",
+ " button.href = '#';\n",
+ " button.title = 'Stop Interaction';\n",
+ " button.innerHTML = '';\n",
+ " button.addEventListener('click', function (_evt) {\n",
+ " fig.handle_close(fig, {});\n",
+ " });\n",
+ " button.addEventListener(\n",
+ " 'mouseover',\n",
+ " on_mouseover_closure('Stop Interaction')\n",
+ " );\n",
+ " buttongrp.appendChild(button);\n",
+ " var titlebar = this.root.querySelector('.ui-dialog-titlebar');\n",
+ " titlebar.insertBefore(buttongrp, titlebar.firstChild);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._remove_fig_handler = function (event) {\n",
+ " var fig = event.data.fig;\n",
+ " if (event.target !== this) {\n",
+ " // Ignore bubbled events from children.\n",
+ " return;\n",
+ " }\n",
+ " fig.close_ws(fig, {});\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._root_extra_style = function (el) {\n",
+ " el.style.boxSizing = 'content-box'; // override notebook setting of border-box.\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._canvas_extra_style = function (el) {\n",
+ " // this is important to make the div 'focusable\n",
+ " el.setAttribute('tabindex', 0);\n",
+ " // reach out to IPython and tell the keyboard manager to turn it's self\n",
+ " // off when our div gets focus\n",
+ "\n",
+ " // location in version 3\n",
+ " if (IPython.notebook.keyboard_manager) {\n",
+ " IPython.notebook.keyboard_manager.register_events(el);\n",
+ " } else {\n",
+ " // location in version 2\n",
+ " IPython.keyboard_manager.register_events(el);\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._key_event_extra = function (event, _name) {\n",
+ " // Check for shift+enter\n",
+ " if (event.shiftKey && event.which === 13) {\n",
+ " this.canvas_div.blur();\n",
+ " // select the cell after this one\n",
+ " var index = IPython.notebook.find_cell_index(this.cell_info[0]);\n",
+ " IPython.notebook.select(index + 1);\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_save = function (fig, _msg) {\n",
+ " fig.ondownload(fig, null);\n",
+ "};\n",
+ "\n",
+ "mpl.find_output_cell = function (html_output) {\n",
+ " // Return the cell and output element which can be found *uniquely* in the notebook.\n",
+ " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n",
+ " // IPython event is triggered only after the cells have been serialised, which for\n",
+ " // our purposes (turning an active figure into a static one), is too late.\n",
+ " var cells = IPython.notebook.get_cells();\n",
+ " var ncells = cells.length;\n",
+ " for (var i = 0; i < ncells; i++) {\n",
+ " var cell = cells[i];\n",
+ " if (cell.cell_type === 'code') {\n",
+ " for (var j = 0; j < cell.output_area.outputs.length; j++) {\n",
+ " var data = cell.output_area.outputs[j];\n",
+ " if (data.data) {\n",
+ " // IPython >= 3 moved mimebundle to data attribute of output\n",
+ " data = data.data;\n",
+ " }\n",
+ " if (data['text/html'] === html_output) {\n",
+ " return [cell, data, j];\n",
+ " }\n",
+ " }\n",
+ " }\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "// Register the function which deals with the matplotlib target/channel.\n",
+ "// The kernel may be null if the page has been refreshed.\n",
+ "if (IPython.notebook.kernel !== null) {\n",
+ " IPython.notebook.kernel.comm_manager.register_target(\n",
+ " 'matplotlib',\n",
+ " mpl.mpl_figure_comm\n",
+ " );\n",
+ "}\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/html": [
+ ""
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/javascript": [
+ "/* Put everything inside the global mpl namespace */\n",
+ "/* global mpl */\n",
+ "window.mpl = {};\n",
+ "\n",
+ "mpl.get_websocket_type = function () {\n",
+ " if (typeof WebSocket !== 'undefined') {\n",
+ " return WebSocket;\n",
+ " } else if (typeof MozWebSocket !== 'undefined') {\n",
+ " return MozWebSocket;\n",
+ " } else {\n",
+ " alert(\n",
+ " 'Your browser does not have WebSocket support. ' +\n",
+ " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n",
+ " 'Firefox 4 and 5 are also supported but you ' +\n",
+ " 'have to enable WebSockets in about:config.'\n",
+ " );\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure = function (figure_id, websocket, ondownload, parent_element) {\n",
+ " this.id = figure_id;\n",
+ "\n",
+ " this.ws = websocket;\n",
+ "\n",
+ " this.supports_binary = this.ws.binaryType !== undefined;\n",
+ "\n",
+ " if (!this.supports_binary) {\n",
+ " var warnings = document.getElementById('mpl-warnings');\n",
+ " if (warnings) {\n",
+ " warnings.style.display = 'block';\n",
+ " warnings.textContent =\n",
+ " 'This browser does not support binary websocket messages. ' +\n",
+ " 'Performance may be slow.';\n",
+ " }\n",
+ " }\n",
+ "\n",
+ " this.imageObj = new Image();\n",
+ "\n",
+ " this.context = undefined;\n",
+ " this.message = undefined;\n",
+ " this.canvas = undefined;\n",
+ " this.rubberband_canvas = undefined;\n",
+ " this.rubberband_context = undefined;\n",
+ " this.format_dropdown = undefined;\n",
+ "\n",
+ " this.image_mode = 'full';\n",
+ "\n",
+ " this.root = document.createElement('div');\n",
+ " this.root.setAttribute('style', 'display: inline-block');\n",
+ " this._root_extra_style(this.root);\n",
+ "\n",
+ " parent_element.appendChild(this.root);\n",
+ "\n",
+ " this._init_header(this);\n",
+ " this._init_canvas(this);\n",
+ " this._init_toolbar(this);\n",
+ "\n",
+ " var fig = this;\n",
+ "\n",
+ " this.waiting = false;\n",
+ "\n",
+ " this.ws.onopen = function () {\n",
+ " fig.send_message('supports_binary', { value: fig.supports_binary });\n",
+ " fig.send_message('send_image_mode', {});\n",
+ " if (fig.ratio !== 1) {\n",
+ " fig.send_message('set_device_pixel_ratio', {\n",
+ " device_pixel_ratio: fig.ratio,\n",
+ " });\n",
+ " }\n",
+ " fig.send_message('refresh', {});\n",
+ " };\n",
+ "\n",
+ " this.imageObj.onload = function () {\n",
+ " if (fig.image_mode === 'full') {\n",
+ " // Full images could contain transparency (where diff images\n",
+ " // almost always do), so we need to clear the canvas so that\n",
+ " // there is no ghosting.\n",
+ " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n",
+ " }\n",
+ " fig.context.drawImage(fig.imageObj, 0, 0);\n",
+ " };\n",
+ "\n",
+ " this.imageObj.onunload = function () {\n",
+ " fig.ws.close();\n",
+ " };\n",
+ "\n",
+ " this.ws.onmessage = this._make_on_message_function(this);\n",
+ "\n",
+ " this.ondownload = ondownload;\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._init_header = function () {\n",
+ " var titlebar = document.createElement('div');\n",
+ " titlebar.classList =\n",
+ " 'ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix';\n",
+ " var titletext = document.createElement('div');\n",
+ " titletext.classList = 'ui-dialog-title';\n",
+ " titletext.setAttribute(\n",
+ " 'style',\n",
+ " 'width: 100%; text-align: center; padding: 3px;'\n",
+ " );\n",
+ " titlebar.appendChild(titletext);\n",
+ " this.root.appendChild(titlebar);\n",
+ " this.header = titletext;\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._canvas_extra_style = function (_canvas_div) {};\n",
+ "\n",
+ "mpl.figure.prototype._root_extra_style = function (_canvas_div) {};\n",
+ "\n",
+ "mpl.figure.prototype._init_canvas = function () {\n",
+ " var fig = this;\n",
+ "\n",
+ " var canvas_div = (this.canvas_div = document.createElement('div'));\n",
+ " canvas_div.setAttribute('tabindex', '0');\n",
+ " canvas_div.setAttribute(\n",
+ " 'style',\n",
+ " 'border: 1px solid #ddd;' +\n",
+ " 'box-sizing: content-box;' +\n",
+ " 'clear: both;' +\n",
+ " 'min-height: 1px;' +\n",
+ " 'min-width: 1px;' +\n",
+ " 'outline: 0;' +\n",
+ " 'overflow: hidden;' +\n",
+ " 'position: relative;' +\n",
+ " 'resize: both;' +\n",
+ " 'z-index: 2;'\n",
+ " );\n",
+ "\n",
+ " function on_keyboard_event_closure(name) {\n",
+ " return function (event) {\n",
+ " return fig.key_event(event, name);\n",
+ " };\n",
+ " }\n",
+ "\n",
+ " canvas_div.addEventListener(\n",
+ " 'keydown',\n",
+ " on_keyboard_event_closure('key_press')\n",
+ " );\n",
+ " canvas_div.addEventListener(\n",
+ " 'keyup',\n",
+ " on_keyboard_event_closure('key_release')\n",
+ " );\n",
+ "\n",
+ " this._canvas_extra_style(canvas_div);\n",
+ " this.root.appendChild(canvas_div);\n",
+ "\n",
+ " var canvas = (this.canvas = document.createElement('canvas'));\n",
+ " canvas.classList.add('mpl-canvas');\n",
+ " canvas.setAttribute(\n",
+ " 'style',\n",
+ " 'box-sizing: content-box;' +\n",
+ " 'pointer-events: none;' +\n",
+ " 'position: relative;' +\n",
+ " 'z-index: 0;'\n",
+ " );\n",
+ "\n",
+ " this.context = canvas.getContext('2d');\n",
+ "\n",
+ " var backingStore =\n",
+ " this.context.backingStorePixelRatio ||\n",
+ " this.context.webkitBackingStorePixelRatio ||\n",
+ " this.context.mozBackingStorePixelRatio ||\n",
+ " this.context.msBackingStorePixelRatio ||\n",
+ " this.context.oBackingStorePixelRatio ||\n",
+ " this.context.backingStorePixelRatio ||\n",
+ " 1;\n",
+ "\n",
+ " this.ratio = (window.devicePixelRatio || 1) / backingStore;\n",
+ "\n",
+ " var rubberband_canvas = (this.rubberband_canvas = document.createElement(\n",
+ " 'canvas'\n",
+ " ));\n",
+ " rubberband_canvas.setAttribute(\n",
+ " 'style',\n",
+ " 'box-sizing: content-box;' +\n",
+ " 'left: 0;' +\n",
+ " 'pointer-events: none;' +\n",
+ " 'position: absolute;' +\n",
+ " 'top: 0;' +\n",
+ " 'z-index: 1;'\n",
+ " );\n",
+ "\n",
+ " // Apply a ponyfill if ResizeObserver is not implemented by browser.\n",
+ " if (this.ResizeObserver === undefined) {\n",
+ " if (window.ResizeObserver !== undefined) {\n",
+ " this.ResizeObserver = window.ResizeObserver;\n",
+ " } else {\n",
+ " var obs = _JSXTOOLS_RESIZE_OBSERVER({});\n",
+ " this.ResizeObserver = obs.ResizeObserver;\n",
+ " }\n",
+ " }\n",
+ "\n",
+ " this.resizeObserverInstance = new this.ResizeObserver(function (entries) {\n",
+ " var nentries = entries.length;\n",
+ " for (var i = 0; i < nentries; i++) {\n",
+ " var entry = entries[i];\n",
+ " var width, height;\n",
+ " if (entry.contentBoxSize) {\n",
+ " if (entry.contentBoxSize instanceof Array) {\n",
+ " // Chrome 84 implements new version of spec.\n",
+ " width = entry.contentBoxSize[0].inlineSize;\n",
+ " height = entry.contentBoxSize[0].blockSize;\n",
+ " } else {\n",
+ " // Firefox implements old version of spec.\n",
+ " width = entry.contentBoxSize.inlineSize;\n",
+ " height = entry.contentBoxSize.blockSize;\n",
+ " }\n",
+ " } else {\n",
+ " // Chrome <84 implements even older version of spec.\n",
+ " width = entry.contentRect.width;\n",
+ " height = entry.contentRect.height;\n",
+ " }\n",
+ "\n",
+ " // Keep the size of the canvas and rubber band canvas in sync with\n",
+ " // the canvas container.\n",
+ " if (entry.devicePixelContentBoxSize) {\n",
+ " // Chrome 84 implements new version of spec.\n",
+ " canvas.setAttribute(\n",
+ " 'width',\n",
+ " entry.devicePixelContentBoxSize[0].inlineSize\n",
+ " );\n",
+ " canvas.setAttribute(\n",
+ " 'height',\n",
+ " entry.devicePixelContentBoxSize[0].blockSize\n",
+ " );\n",
+ " } else {\n",
+ " canvas.setAttribute('width', width * fig.ratio);\n",
+ " canvas.setAttribute('height', height * fig.ratio);\n",
+ " }\n",
+ " /* This rescales the canvas back to display pixels, so that it\n",
+ " * appears correct on HiDPI screens. */\n",
+ " canvas.style.width = width + 'px';\n",
+ " canvas.style.height = height + 'px';\n",
+ "\n",
+ " rubberband_canvas.setAttribute('width', width);\n",
+ " rubberband_canvas.setAttribute('height', height);\n",
+ "\n",
+ " // And update the size in Python. We ignore the initial 0/0 size\n",
+ " // that occurs as the element is placed into the DOM, which should\n",
+ " // otherwise not happen due to the minimum size styling.\n",
+ " if (fig.ws.readyState == 1 && width != 0 && height != 0) {\n",
+ " fig.request_resize(width, height);\n",
+ " }\n",
+ " }\n",
+ " });\n",
+ " this.resizeObserverInstance.observe(canvas_div);\n",
+ "\n",
+ " function on_mouse_event_closure(name) {\n",
+ " /* User Agent sniffing is bad, but WebKit is busted:\n",
+ " * https://bugs.webkit.org/show_bug.cgi?id=144526\n",
+ " * https://bugs.webkit.org/show_bug.cgi?id=181818\n",
+ " * The worst that happens here is that they get an extra browser\n",
+ " * selection when dragging, if this check fails to catch them.\n",
+ " */\n",
+ " var UA = navigator.userAgent;\n",
+ " var isWebKit = /AppleWebKit/.test(UA) && !/Chrome/.test(UA);\n",
+ " if(isWebKit) {\n",
+ " return function (event) {\n",
+ " /* This prevents the web browser from automatically changing to\n",
+ " * the text insertion cursor when the button is pressed. We\n",
+ " * want to control all of the cursor setting manually through\n",
+ " * the 'cursor' event from matplotlib */\n",
+ " event.preventDefault()\n",
+ " return fig.mouse_event(event, name);\n",
+ " };\n",
+ " } else {\n",
+ " return function (event) {\n",
+ " return fig.mouse_event(event, name);\n",
+ " };\n",
+ " }\n",
+ " }\n",
+ "\n",
+ " canvas_div.addEventListener(\n",
+ " 'mousedown',\n",
+ " on_mouse_event_closure('button_press')\n",
+ " );\n",
+ " canvas_div.addEventListener(\n",
+ " 'mouseup',\n",
+ " on_mouse_event_closure('button_release')\n",
+ " );\n",
+ " canvas_div.addEventListener(\n",
+ " 'dblclick',\n",
+ " on_mouse_event_closure('dblclick')\n",
+ " );\n",
+ " // Throttle sequential mouse events to 1 every 20ms.\n",
+ " canvas_div.addEventListener(\n",
+ " 'mousemove',\n",
+ " on_mouse_event_closure('motion_notify')\n",
+ " );\n",
+ "\n",
+ " canvas_div.addEventListener(\n",
+ " 'mouseenter',\n",
+ " on_mouse_event_closure('figure_enter')\n",
+ " );\n",
+ " canvas_div.addEventListener(\n",
+ " 'mouseleave',\n",
+ " on_mouse_event_closure('figure_leave')\n",
+ " );\n",
+ "\n",
+ " canvas_div.addEventListener('wheel', function (event) {\n",
+ " if (event.deltaY < 0) {\n",
+ " event.step = 1;\n",
+ " } else {\n",
+ " event.step = -1;\n",
+ " }\n",
+ " on_mouse_event_closure('scroll')(event);\n",
+ " });\n",
+ "\n",
+ " canvas_div.appendChild(canvas);\n",
+ " canvas_div.appendChild(rubberband_canvas);\n",
+ "\n",
+ " this.rubberband_context = rubberband_canvas.getContext('2d');\n",
+ " this.rubberband_context.strokeStyle = '#000000';\n",
+ "\n",
+ " this._resize_canvas = function (width, height, forward) {\n",
+ " if (forward) {\n",
+ " canvas_div.style.width = width + 'px';\n",
+ " canvas_div.style.height = height + 'px';\n",
+ " }\n",
+ " };\n",
+ "\n",
+ " // Disable right mouse context menu.\n",
+ " canvas_div.addEventListener('contextmenu', function (_e) {\n",
+ " event.preventDefault();\n",
+ " return false;\n",
+ " });\n",
+ "\n",
+ " function set_focus() {\n",
+ " canvas.focus();\n",
+ " canvas_div.focus();\n",
+ " }\n",
+ "\n",
+ " window.setTimeout(set_focus, 100);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._init_toolbar = function () {\n",
+ " var fig = this;\n",
+ "\n",
+ " var toolbar = document.createElement('div');\n",
+ " toolbar.classList = 'mpl-toolbar';\n",
+ " this.root.appendChild(toolbar);\n",
+ "\n",
+ " function on_click_closure(name) {\n",
+ " return function (_event) {\n",
+ " return fig.toolbar_button_onclick(name);\n",
+ " };\n",
+ " }\n",
+ "\n",
+ " function on_mouseover_closure(tooltip) {\n",
+ " return function (event) {\n",
+ " if (!event.currentTarget.disabled) {\n",
+ " return fig.toolbar_button_onmouseover(tooltip);\n",
+ " }\n",
+ " };\n",
+ " }\n",
+ "\n",
+ " fig.buttons = {};\n",
+ " var buttonGroup = document.createElement('div');\n",
+ " buttonGroup.classList = 'mpl-button-group';\n",
+ " for (var toolbar_ind in mpl.toolbar_items) {\n",
+ " var name = mpl.toolbar_items[toolbar_ind][0];\n",
+ " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
+ " var image = mpl.toolbar_items[toolbar_ind][2];\n",
+ " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
+ "\n",
+ " if (!name) {\n",
+ " /* Instead of a spacer, we start a new button group. */\n",
+ " if (buttonGroup.hasChildNodes()) {\n",
+ " toolbar.appendChild(buttonGroup);\n",
+ " }\n",
+ " buttonGroup = document.createElement('div');\n",
+ " buttonGroup.classList = 'mpl-button-group';\n",
+ " continue;\n",
+ " }\n",
+ "\n",
+ " var button = (fig.buttons[name] = document.createElement('button'));\n",
+ " button.classList = 'mpl-widget';\n",
+ " button.setAttribute('role', 'button');\n",
+ " button.setAttribute('aria-disabled', 'false');\n",
+ " button.addEventListener('click', on_click_closure(method_name));\n",
+ " button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n",
+ "\n",
+ " var icon_img = document.createElement('img');\n",
+ " icon_img.src = '_images/' + image + '.png';\n",
+ " icon_img.srcset = '_images/' + image + '_large.png 2x';\n",
+ " icon_img.alt = tooltip;\n",
+ " button.appendChild(icon_img);\n",
+ "\n",
+ " buttonGroup.appendChild(button);\n",
+ " }\n",
+ "\n",
+ " if (buttonGroup.hasChildNodes()) {\n",
+ " toolbar.appendChild(buttonGroup);\n",
+ " }\n",
+ "\n",
+ " var fmt_picker = document.createElement('select');\n",
+ " fmt_picker.classList = 'mpl-widget';\n",
+ " toolbar.appendChild(fmt_picker);\n",
+ " this.format_dropdown = fmt_picker;\n",
+ "\n",
+ " for (var ind in mpl.extensions) {\n",
+ " var fmt = mpl.extensions[ind];\n",
+ " var option = document.createElement('option');\n",
+ " option.selected = fmt === mpl.default_extension;\n",
+ " option.innerHTML = fmt;\n",
+ " fmt_picker.appendChild(option);\n",
+ " }\n",
+ "\n",
+ " var status_bar = document.createElement('span');\n",
+ " status_bar.classList = 'mpl-message';\n",
+ " toolbar.appendChild(status_bar);\n",
+ " this.message = status_bar;\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.request_resize = function (x_pixels, y_pixels) {\n",
+ " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n",
+ " // which will in turn request a refresh of the image.\n",
+ " this.send_message('resize', { width: x_pixels, height: y_pixels });\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.send_message = function (type, properties) {\n",
+ " properties['type'] = type;\n",
+ " properties['figure_id'] = this.id;\n",
+ " this.ws.send(JSON.stringify(properties));\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.send_draw_message = function () {\n",
+ " if (!this.waiting) {\n",
+ " this.waiting = true;\n",
+ " this.ws.send(JSON.stringify({ type: 'draw', figure_id: this.id }));\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_save = function (fig, _msg) {\n",
+ " var format_dropdown = fig.format_dropdown;\n",
+ " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n",
+ " fig.ondownload(fig, format);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_resize = function (fig, msg) {\n",
+ " var size = msg['size'];\n",
+ " if (size[0] !== fig.canvas.width || size[1] !== fig.canvas.height) {\n",
+ " fig._resize_canvas(size[0], size[1], msg['forward']);\n",
+ " fig.send_message('refresh', {});\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_rubberband = function (fig, msg) {\n",
+ " var x0 = msg['x0'] / fig.ratio;\n",
+ " var y0 = (fig.canvas.height - msg['y0']) / fig.ratio;\n",
+ " var x1 = msg['x1'] / fig.ratio;\n",
+ " var y1 = (fig.canvas.height - msg['y1']) / fig.ratio;\n",
+ " x0 = Math.floor(x0) + 0.5;\n",
+ " y0 = Math.floor(y0) + 0.5;\n",
+ " x1 = Math.floor(x1) + 0.5;\n",
+ " y1 = Math.floor(y1) + 0.5;\n",
+ " var min_x = Math.min(x0, x1);\n",
+ " var min_y = Math.min(y0, y1);\n",
+ " var width = Math.abs(x1 - x0);\n",
+ " var height = Math.abs(y1 - y0);\n",
+ "\n",
+ " fig.rubberband_context.clearRect(\n",
+ " 0,\n",
+ " 0,\n",
+ " fig.canvas.width / fig.ratio,\n",
+ " fig.canvas.height / fig.ratio\n",
+ " );\n",
+ "\n",
+ " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_figure_label = function (fig, msg) {\n",
+ " // Updates the figure title.\n",
+ " fig.header.textContent = msg['label'];\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_cursor = function (fig, msg) {\n",
+ " fig.canvas_div.style.cursor = msg['cursor'];\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_message = function (fig, msg) {\n",
+ " fig.message.textContent = msg['message'];\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_draw = function (fig, _msg) {\n",
+ " // Request the server to send over a new figure.\n",
+ " fig.send_draw_message();\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_image_mode = function (fig, msg) {\n",
+ " fig.image_mode = msg['mode'];\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_history_buttons = function (fig, msg) {\n",
+ " for (var key in msg) {\n",
+ " if (!(key in fig.buttons)) {\n",
+ " continue;\n",
+ " }\n",
+ " fig.buttons[key].disabled = !msg[key];\n",
+ " fig.buttons[key].setAttribute('aria-disabled', !msg[key]);\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_navigate_mode = function (fig, msg) {\n",
+ " if (msg['mode'] === 'PAN') {\n",
+ " fig.buttons['Pan'].classList.add('active');\n",
+ " fig.buttons['Zoom'].classList.remove('active');\n",
+ " } else if (msg['mode'] === 'ZOOM') {\n",
+ " fig.buttons['Pan'].classList.remove('active');\n",
+ " fig.buttons['Zoom'].classList.add('active');\n",
+ " } else {\n",
+ " fig.buttons['Pan'].classList.remove('active');\n",
+ " fig.buttons['Zoom'].classList.remove('active');\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.updated_canvas_event = function () {\n",
+ " // Called whenever the canvas gets updated.\n",
+ " this.send_message('ack', {});\n",
+ "};\n",
+ "\n",
+ "// A function to construct a web socket function for onmessage handling.\n",
+ "// Called in the figure constructor.\n",
+ "mpl.figure.prototype._make_on_message_function = function (fig) {\n",
+ " return function socket_on_message(evt) {\n",
+ " if (evt.data instanceof Blob) {\n",
+ " var img = evt.data;\n",
+ " if (img.type !== 'image/png') {\n",
+ " /* FIXME: We get \"Resource interpreted as Image but\n",
+ " * transferred with MIME type text/plain:\" errors on\n",
+ " * Chrome. But how to set the MIME type? It doesn't seem\n",
+ " * to be part of the websocket stream */\n",
+ " img.type = 'image/png';\n",
+ " }\n",
+ "\n",
+ " /* Free the memory for the previous frames */\n",
+ " if (fig.imageObj.src) {\n",
+ " (window.URL || window.webkitURL).revokeObjectURL(\n",
+ " fig.imageObj.src\n",
+ " );\n",
+ " }\n",
+ "\n",
+ " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
+ " img\n",
+ " );\n",
+ " fig.updated_canvas_event();\n",
+ " fig.waiting = false;\n",
+ " return;\n",
+ " } else if (\n",
+ " typeof evt.data === 'string' &&\n",
+ " evt.data.slice(0, 21) === 'data:image/png;base64'\n",
+ " ) {\n",
+ " fig.imageObj.src = evt.data;\n",
+ " fig.updated_canvas_event();\n",
+ " fig.waiting = false;\n",
+ " return;\n",
+ " }\n",
+ "\n",
+ " var msg = JSON.parse(evt.data);\n",
+ " var msg_type = msg['type'];\n",
+ "\n",
+ " // Call the \"handle_{type}\" callback, which takes\n",
+ " // the figure and JSON message as its only arguments.\n",
+ " try {\n",
+ " var callback = fig['handle_' + msg_type];\n",
+ " } catch (e) {\n",
+ " console.log(\n",
+ " \"No handler for the '\" + msg_type + \"' message type: \",\n",
+ " msg\n",
+ " );\n",
+ " return;\n",
+ " }\n",
+ "\n",
+ " if (callback) {\n",
+ " try {\n",
+ " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n",
+ " callback(fig, msg);\n",
+ " } catch (e) {\n",
+ " console.log(\n",
+ " \"Exception inside the 'handler_\" + msg_type + \"' callback:\",\n",
+ " e,\n",
+ " e.stack,\n",
+ " msg\n",
+ " );\n",
+ " }\n",
+ " }\n",
+ " };\n",
+ "};\n",
+ "\n",
+ "function getModifiers(event) {\n",
+ " var mods = [];\n",
+ " if (event.ctrlKey) {\n",
+ " mods.push('ctrl');\n",
+ " }\n",
+ " if (event.altKey) {\n",
+ " mods.push('alt');\n",
+ " }\n",
+ " if (event.shiftKey) {\n",
+ " mods.push('shift');\n",
+ " }\n",
+ " if (event.metaKey) {\n",
+ " mods.push('meta');\n",
+ " }\n",
+ " return mods;\n",
+ "}\n",
+ "\n",
+ "/*\n",
+ " * return a copy of an object with only non-object keys\n",
+ " * we need this to avoid circular references\n",
+ " * https://stackoverflow.com/a/24161582/3208463\n",
+ " */\n",
+ "function simpleKeys(original) {\n",
+ " return Object.keys(original).reduce(function (obj, key) {\n",
+ " if (typeof original[key] !== 'object') {\n",
+ " obj[key] = original[key];\n",
+ " }\n",
+ " return obj;\n",
+ " }, {});\n",
+ "}\n",
+ "\n",
+ "mpl.figure.prototype.mouse_event = function (event, name) {\n",
+ " if (name === 'button_press') {\n",
+ " this.canvas.focus();\n",
+ " this.canvas_div.focus();\n",
+ " }\n",
+ "\n",
+ " // from https://stackoverflow.com/q/1114465\n",
+ " var boundingRect = this.canvas.getBoundingClientRect();\n",
+ " var x = (event.clientX - boundingRect.left) * this.ratio;\n",
+ " var y = (event.clientY - boundingRect.top) * this.ratio;\n",
+ "\n",
+ " this.send_message(name, {\n",
+ " x: x,\n",
+ " y: y,\n",
+ " button: event.button,\n",
+ " step: event.step,\n",
+ " modifiers: getModifiers(event),\n",
+ " guiEvent: simpleKeys(event),\n",
+ " });\n",
+ "\n",
+ " return false;\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._key_event_extra = function (_event, _name) {\n",
+ " // Handle any extra behaviour associated with a key event\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.key_event = function (event, name) {\n",
+ " // Prevent repeat events\n",
+ " if (name === 'key_press') {\n",
+ " if (event.key === this._key) {\n",
+ " return;\n",
+ " } else {\n",
+ " this._key = event.key;\n",
+ " }\n",
+ " }\n",
+ " if (name === 'key_release') {\n",
+ " this._key = null;\n",
+ " }\n",
+ "\n",
+ " var value = '';\n",
+ " if (event.ctrlKey && event.key !== 'Control') {\n",
+ " value += 'ctrl+';\n",
+ " }\n",
+ " else if (event.altKey && event.key !== 'Alt') {\n",
+ " value += 'alt+';\n",
+ " }\n",
+ " else if (event.shiftKey && event.key !== 'Shift') {\n",
+ " value += 'shift+';\n",
+ " }\n",
+ "\n",
+ " value += 'k' + event.key;\n",
+ "\n",
+ " this._key_event_extra(event, name);\n",
+ "\n",
+ " this.send_message(name, { key: value, guiEvent: simpleKeys(event) });\n",
+ " return false;\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.toolbar_button_onclick = function (name) {\n",
+ " if (name === 'download') {\n",
+ " this.handle_save(this, null);\n",
+ " } else {\n",
+ " this.send_message('toolbar_button', { name: name });\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.toolbar_button_onmouseover = function (tooltip) {\n",
+ " this.message.textContent = tooltip;\n",
+ "};\n",
+ "\n",
+ "///////////////// REMAINING CONTENT GENERATED BY embed_js.py /////////////////\n",
+ "// prettier-ignore\n",
+ "var _JSXTOOLS_RESIZE_OBSERVER=function(A){var t,i=new WeakMap,n=new WeakMap,a=new WeakMap,r=new WeakMap,o=new Set;function s(e){if(!(this instanceof s))throw new TypeError(\"Constructor requires 'new' operator\");i.set(this,e)}function h(){throw new TypeError(\"Function is not a constructor\")}function c(e,t,i,n){e=0 in arguments?Number(arguments[0]):0,t=1 in arguments?Number(arguments[1]):0,i=2 in arguments?Number(arguments[2]):0,n=3 in arguments?Number(arguments[3]):0,this.right=(this.x=this.left=e)+(this.width=i),this.bottom=(this.y=this.top=t)+(this.height=n),Object.freeze(this)}function d(){t=requestAnimationFrame(d);var s=new WeakMap,p=new Set;o.forEach((function(t){r.get(t).forEach((function(i){var r=t instanceof window.SVGElement,o=a.get(t),d=r?0:parseFloat(o.paddingTop),f=r?0:parseFloat(o.paddingRight),l=r?0:parseFloat(o.paddingBottom),u=r?0:parseFloat(o.paddingLeft),g=r?0:parseFloat(o.borderTopWidth),m=r?0:parseFloat(o.borderRightWidth),w=r?0:parseFloat(o.borderBottomWidth),b=u+f,F=d+l,v=(r?0:parseFloat(o.borderLeftWidth))+m,W=g+w,y=r?0:t.offsetHeight-W-t.clientHeight,E=r?0:t.offsetWidth-v-t.clientWidth,R=b+v,z=F+W,M=r?t.width:parseFloat(o.width)-R-E,O=r?t.height:parseFloat(o.height)-z-y;if(n.has(t)){var k=n.get(t);if(k[0]===M&&k[1]===O)return}n.set(t,[M,O]);var S=Object.create(h.prototype);S.target=t,S.contentRect=new c(u,d,M,O),s.has(i)||(s.set(i,[]),p.add(i)),s.get(i).push(S)}))})),p.forEach((function(e){i.get(e).call(e,s.get(e),e)}))}return s.prototype.observe=function(i){if(i instanceof window.Element){r.has(i)||(r.set(i,new Set),o.add(i),a.set(i,window.getComputedStyle(i)));var n=r.get(i);n.has(this)||n.add(this),cancelAnimationFrame(t),t=requestAnimationFrame(d)}},s.prototype.unobserve=function(i){if(i instanceof window.Element&&r.has(i)){var n=r.get(i);n.has(this)&&(n.delete(this),n.size||(r.delete(i),o.delete(i))),n.size||r.delete(i),o.size||cancelAnimationFrame(t)}},A.DOMRectReadOnly=c,A.ResizeObserver=s,A.ResizeObserverEntry=h,A}; // eslint-disable-line\n",
+ "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Left button pans, Right button zooms\\nx/y fixes axis, CTRL fixes aspect\", \"fa fa-arrows\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\\nx/y fixes axis\", \"fa fa-square-o\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o\", \"download\"]];\n",
+ "\n",
+ "mpl.extensions = [\"eps\", \"jpeg\", \"pgf\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\", \"tif\", \"webp\"];\n",
+ "\n",
+ "mpl.default_extension = \"png\";/* global mpl */\n",
+ "\n",
+ "var comm_websocket_adapter = function (comm) {\n",
+ " // Create a \"websocket\"-like object which calls the given IPython comm\n",
+ " // object with the appropriate methods. Currently this is a non binary\n",
+ " // socket, so there is still some room for performance tuning.\n",
+ " var ws = {};\n",
+ "\n",
+ " ws.binaryType = comm.kernel.ws.binaryType;\n",
+ " ws.readyState = comm.kernel.ws.readyState;\n",
+ " function updateReadyState(_event) {\n",
+ " if (comm.kernel.ws) {\n",
+ " ws.readyState = comm.kernel.ws.readyState;\n",
+ " } else {\n",
+ " ws.readyState = 3; // Closed state.\n",
+ " }\n",
+ " }\n",
+ " comm.kernel.ws.addEventListener('open', updateReadyState);\n",
+ " comm.kernel.ws.addEventListener('close', updateReadyState);\n",
+ " comm.kernel.ws.addEventListener('error', updateReadyState);\n",
+ "\n",
+ " ws.close = function () {\n",
+ " comm.close();\n",
+ " };\n",
+ " ws.send = function (m) {\n",
+ " //console.log('sending', m);\n",
+ " comm.send(m);\n",
+ " };\n",
+ " // Register the callback with on_msg.\n",
+ " comm.on_msg(function (msg) {\n",
+ " //console.log('receiving', msg['content']['data'], msg);\n",
+ " var data = msg['content']['data'];\n",
+ " if (data['blob'] !== undefined) {\n",
+ " data = {\n",
+ " data: new Blob(msg['buffers'], { type: data['blob'] }),\n",
+ " };\n",
+ " }\n",
+ " // Pass the mpl event to the overridden (by mpl) onmessage function.\n",
+ " ws.onmessage(data);\n",
+ " });\n",
+ " return ws;\n",
+ "};\n",
+ "\n",
+ "mpl.mpl_figure_comm = function (comm, msg) {\n",
+ " // This is the function which gets called when the mpl process\n",
+ " // starts-up an IPython Comm through the \"matplotlib\" channel.\n",
+ "\n",
+ " var id = msg.content.data.id;\n",
+ " // Get hold of the div created by the display call when the Comm\n",
+ " // socket was opened in Python.\n",
+ " var element = document.getElementById(id);\n",
+ " var ws_proxy = comm_websocket_adapter(comm);\n",
+ "\n",
+ " function ondownload(figure, _format) {\n",
+ " window.open(figure.canvas.toDataURL());\n",
+ " }\n",
+ "\n",
+ " var fig = new mpl.figure(id, ws_proxy, ondownload, element);\n",
+ "\n",
+ " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n",
+ " // web socket which is closed, not our websocket->open comm proxy.\n",
+ " ws_proxy.onopen();\n",
+ "\n",
+ " fig.parent_element = element;\n",
+ " fig.cell_info = mpl.find_output_cell(\"\");\n",
+ " if (!fig.cell_info) {\n",
+ " console.error('Failed to find cell for figure', id, fig);\n",
+ " return;\n",
+ " }\n",
+ " fig.cell_info[0].output_area.element.on(\n",
+ " 'cleared',\n",
+ " { fig: fig },\n",
+ " fig._remove_fig_handler\n",
+ " );\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_close = function (fig, msg) {\n",
+ " var width = fig.canvas.width / fig.ratio;\n",
+ " fig.cell_info[0].output_area.element.off(\n",
+ " 'cleared',\n",
+ " fig._remove_fig_handler\n",
+ " );\n",
+ " fig.resizeObserverInstance.unobserve(fig.canvas_div);\n",
+ "\n",
+ " // Update the output cell to use the data from the current canvas.\n",
+ " fig.push_to_output();\n",
+ " var dataURL = fig.canvas.toDataURL();\n",
+ " // Re-enable the keyboard manager in IPython - without this line, in FF,\n",
+ " // the notebook keyboard shortcuts fail.\n",
+ " IPython.keyboard_manager.enable();\n",
+ " fig.parent_element.innerHTML =\n",
+ " '';\n",
+ " fig.close_ws(fig, msg);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.close_ws = function (fig, msg) {\n",
+ " fig.send_message('closing', msg);\n",
+ " // fig.ws.close()\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.push_to_output = function (_remove_interactive) {\n",
+ " // Turn the data on the canvas into data in the output cell.\n",
+ " var width = this.canvas.width / this.ratio;\n",
+ " var dataURL = this.canvas.toDataURL();\n",
+ " this.cell_info[1]['text/html'] =\n",
+ " '';\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.updated_canvas_event = function () {\n",
+ " // Tell IPython that the notebook contents must change.\n",
+ " IPython.notebook.set_dirty(true);\n",
+ " this.send_message('ack', {});\n",
+ " var fig = this;\n",
+ " // Wait a second, then push the new image to the DOM so\n",
+ " // that it is saved nicely (might be nice to debounce this).\n",
+ " setTimeout(function () {\n",
+ " fig.push_to_output();\n",
+ " }, 1000);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._init_toolbar = function () {\n",
+ " var fig = this;\n",
+ "\n",
+ " var toolbar = document.createElement('div');\n",
+ " toolbar.classList = 'btn-toolbar';\n",
+ " this.root.appendChild(toolbar);\n",
+ "\n",
+ " function on_click_closure(name) {\n",
+ " return function (_event) {\n",
+ " return fig.toolbar_button_onclick(name);\n",
+ " };\n",
+ " }\n",
+ "\n",
+ " function on_mouseover_closure(tooltip) {\n",
+ " return function (event) {\n",
+ " if (!event.currentTarget.disabled) {\n",
+ " return fig.toolbar_button_onmouseover(tooltip);\n",
+ " }\n",
+ " };\n",
+ " }\n",
+ "\n",
+ " fig.buttons = {};\n",
+ " var buttonGroup = document.createElement('div');\n",
+ " buttonGroup.classList = 'btn-group';\n",
+ " var button;\n",
+ " for (var toolbar_ind in mpl.toolbar_items) {\n",
+ " var name = mpl.toolbar_items[toolbar_ind][0];\n",
+ " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n",
+ " var image = mpl.toolbar_items[toolbar_ind][2];\n",
+ " var method_name = mpl.toolbar_items[toolbar_ind][3];\n",
+ "\n",
+ " if (!name) {\n",
+ " /* Instead of a spacer, we start a new button group. */\n",
+ " if (buttonGroup.hasChildNodes()) {\n",
+ " toolbar.appendChild(buttonGroup);\n",
+ " }\n",
+ " buttonGroup = document.createElement('div');\n",
+ " buttonGroup.classList = 'btn-group';\n",
+ " continue;\n",
+ " }\n",
+ "\n",
+ " button = fig.buttons[name] = document.createElement('button');\n",
+ " button.classList = 'btn btn-default';\n",
+ " button.href = '#';\n",
+ " button.title = name;\n",
+ " button.innerHTML = '';\n",
+ " button.addEventListener('click', on_click_closure(method_name));\n",
+ " button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n",
+ " buttonGroup.appendChild(button);\n",
+ " }\n",
+ "\n",
+ " if (buttonGroup.hasChildNodes()) {\n",
+ " toolbar.appendChild(buttonGroup);\n",
+ " }\n",
+ "\n",
+ " // Add the status bar.\n",
+ " var status_bar = document.createElement('span');\n",
+ " status_bar.classList = 'mpl-message pull-right';\n",
+ " toolbar.appendChild(status_bar);\n",
+ " this.message = status_bar;\n",
+ "\n",
+ " // Add the close button to the window.\n",
+ " var buttongrp = document.createElement('div');\n",
+ " buttongrp.classList = 'btn-group inline pull-right';\n",
+ " button = document.createElement('button');\n",
+ " button.classList = 'btn btn-mini btn-primary';\n",
+ " button.href = '#';\n",
+ " button.title = 'Stop Interaction';\n",
+ " button.innerHTML = '';\n",
+ " button.addEventListener('click', function (_evt) {\n",
+ " fig.handle_close(fig, {});\n",
+ " });\n",
+ " button.addEventListener(\n",
+ " 'mouseover',\n",
+ " on_mouseover_closure('Stop Interaction')\n",
+ " );\n",
+ " buttongrp.appendChild(button);\n",
+ " var titlebar = this.root.querySelector('.ui-dialog-titlebar');\n",
+ " titlebar.insertBefore(buttongrp, titlebar.firstChild);\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._remove_fig_handler = function (event) {\n",
+ " var fig = event.data.fig;\n",
+ " if (event.target !== this) {\n",
+ " // Ignore bubbled events from children.\n",
+ " return;\n",
+ " }\n",
+ " fig.close_ws(fig, {});\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._root_extra_style = function (el) {\n",
+ " el.style.boxSizing = 'content-box'; // override notebook setting of border-box.\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._canvas_extra_style = function (el) {\n",
+ " // this is important to make the div 'focusable\n",
+ " el.setAttribute('tabindex', 0);\n",
+ " // reach out to IPython and tell the keyboard manager to turn it's self\n",
+ " // off when our div gets focus\n",
+ "\n",
+ " // location in version 3\n",
+ " if (IPython.notebook.keyboard_manager) {\n",
+ " IPython.notebook.keyboard_manager.register_events(el);\n",
+ " } else {\n",
+ " // location in version 2\n",
+ " IPython.keyboard_manager.register_events(el);\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype._key_event_extra = function (event, _name) {\n",
+ " // Check for shift+enter\n",
+ " if (event.shiftKey && event.which === 13) {\n",
+ " this.canvas_div.blur();\n",
+ " // select the cell after this one\n",
+ " var index = IPython.notebook.find_cell_index(this.cell_info[0]);\n",
+ " IPython.notebook.select(index + 1);\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "mpl.figure.prototype.handle_save = function (fig, _msg) {\n",
+ " fig.ondownload(fig, null);\n",
+ "};\n",
+ "\n",
+ "mpl.find_output_cell = function (html_output) {\n",
+ " // Return the cell and output element which can be found *uniquely* in the notebook.\n",
+ " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n",
+ " // IPython event is triggered only after the cells have been serialised, which for\n",
+ " // our purposes (turning an active figure into a static one), is too late.\n",
+ " var cells = IPython.notebook.get_cells();\n",
+ " var ncells = cells.length;\n",
+ " for (var i = 0; i < ncells; i++) {\n",
+ " var cell = cells[i];\n",
+ " if (cell.cell_type === 'code') {\n",
+ " for (var j = 0; j < cell.output_area.outputs.length; j++) {\n",
+ " var data = cell.output_area.outputs[j];\n",
+ " if (data.data) {\n",
+ " // IPython >= 3 moved mimebundle to data attribute of output\n",
+ " data = data.data;\n",
+ " }\n",
+ " if (data['text/html'] === html_output) {\n",
+ " return [cell, data, j];\n",
+ " }\n",
+ " }\n",
+ " }\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "// Register the function which deals with the matplotlib target/channel.\n",
+ "// The kernel may be null if the page has been refreshed.\n",
+ "if (IPython.notebook.kernel !== null) {\n",
+ " IPython.notebook.kernel.comm_manager.register_target(\n",
+ " 'matplotlib',\n",
+ " mpl.mpl_figure_comm\n",
+ " );\n",
+ "}\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/html": [
+ ""
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "viz.plot_rank_histogram(xp.stats)\n",
+ "viz.plot_err_components(xp.stats)\n",
+ "viz.plot_hovmoller(xx)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "304273d1",
+ "metadata": {},
+ "source": [
+ "### Excercise (methods)\n",
+ "- Try out each of the above DA methods (currently commented out).\n",
+ "- Next, remove the call to `replay`, and set `liveplots=False` above.\n",
+ "- Now, use the iterative EnKS (`iEnKS`), and try to find a parameter combination\n",
+ " for it so that you achieve a lower `rmse.a` than with the `PartFilt`.\n",
+ "\n",
+ "*Hint*: In general, there is no free lunch. Similarly, not all methods work\n",
+ "for all problems; additionally, methods often have parameters that require\n",
+ "tuning. Luckily, in DAPPER, you should be able to find suitably tuned\n",
+ "configuration settings for various DA methods *in the files that define the\n",
+ "HMM*. If you do not find a suggested configuration for a given method, you\n",
+ "will have to tune it yourself. The example script `basic_2` shows how DAPPER\n",
+ "facilitates the tuning process, and `basic_3` takes this further."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "de2e51e4",
+ "metadata": {},
+ "source": [
+ "### Excercise (models)\n",
+ "Run an experiment for each of these models\n",
+ "\n",
+ "- LotkaVolterra\n",
+ "- Lorenz96\n",
+ "- LA\n",
+ "- QG"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "bee8dfc6",
+ "metadata": {},
+ "source": [
+ "### Excercise (diagnostics)\n",
+ "- Create a new code cell, and copy-paste the above `print(...tabulate)`\n",
+ " command into it. Then, replace `rmse` by `err.rms`. This should yield\n",
+ " the same printout, as is merely an abbreviation of the latter.\n",
+ "- Next, figure out how to print the time average *forecast (i.e. prior)* error\n",
+ " (and `rmv`) instead. Explain (in broad terms) why the values are larger than\n",
+ " for the *analysis* values.\n",
+ "- Finally, instead of the `rms` spatial/field averages,\n",
+ " print the regular mean (`.m`) averages. Explain why `err.m` is nearly zero,\n",
+ " in contrast to `err.rms`."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ad85889d",
+ "metadata": {},
+ "source": [
+ "### Excercise (memory)\n",
+ "Why are the replay plots not as smooth as the liveplot?\n",
+ "\n",
+ "*Hint*: provide the keyword `store_u=True` to `assimilate()` to avoid this."
+ ]
+ }
+ ],
+ "metadata": {
+ "jupytext": {
+ "cell_metadata_filter": "-all",
+ "formats": "py:light,ipynb",
+ "main_language": "python",
+ "notebook_metadata_filter": "-all"
+ },
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.12.4"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/examples/basic_1.py b/docs/examples/basic_1.py
similarity index 55%
rename from examples/basic_1.py
rename to docs/examples/basic_1.py
index 41df9f09..f4577901 100644
--- a/examples/basic_1.py
+++ b/docs/examples/basic_1.py
@@ -1,77 +1,78 @@
-# ## Illustrate usage of DAPPER to (interactively) run a synthetic ("twin") experiment.
+# ## A single, interactive, synthetic ("twin") experiment
+# If run as a script in a terminal (i.e. not as a jupyter notebook)
+# then the liveplotting is *interactive* (can be paused, skipped, etc).
-# #### Imports
+# ### Imports
# NB: On Gooble Colab,
# *replace* `%matplotlib notebook` (right below) by\
# `!python -m pip install git+https://github.com/nansencenter/DAPPER.git`
# %matplotlib notebook
-from mpl_tools import is_notebook_or_qt as nb
import dapper as dpr
import dapper.da_methods as da
-# #### Load experiment setup: the hidden Markov model (HMM)
-from dapper.mods.Lorenz63.sakov2012 import HMM
+# Generate the same random numbers each time this script is run:
+
+dpr.set_seed(3000);
-# #### Generate the same random numbers each time this script is run
+# ### Load experiment setup: the hidden Markov model (HMM)
-dpr.set_seed(3000)
+from dapper.mods.Lorenz63.sakov2012 import HMM
+HMM # ⇒ printout (in notebooks)
-# #### Simulate synthetic truth (xx) and noisy obs (yy)
+# ### Simulate synthetic truth (xx) and noisy obs (yy)
+# A variable named `` conventionally refers to a *time series* of ``.
HMM.tseq.T = 30 # shorten experiment
xx, yy = HMM.simulate()
-# #### Specify a DA method configuration ("xp" is short for "experiment")
+# ### Specify a DA method
+# Here "xp" is short for "experiment" configuration.
# xp = da.OptInterp()
# xp = da.Var3D()
# xp = da.ExtKF(infl=90)
xp = da.EnKF("Sqrt", N=10, infl=1.02, rot=True)
# xp = da.PartFilt(N=100, reg=2.4, NER=0.3)
+xp # ⇒ printout (in notebooks)
-# #### Assimilate yy, knowing the HMM; xx is used to assess the performance
+# ### Assimilate yy
+# Note that the assimilation "knows" the HMM,
+# but `xx` is only used for performance *assessment*.
-xp.assimilate(HMM, xx, yy, liveplots=not nb)
+xp.assimilate(HMM, xx, yy)
-# #### Average the time series of various statistics
+# ### Average the time series
+# Computes a set of different statistics.
# print(xp.stats) # ⇒ long printout
xp.stats.average_in_time()
-# #### Print some of these time-averages
+# Print some of these time-averages
# print(xp.avrgs) # ⇒ long printout
print(xp.avrgs.tabulate(["rmse.a", "rmv.a"]))
-# #### Replay liveplotters
+# ### Replay liveplotters
xp.stats.replay(
# speed=.6 # `speed` does not work in notebooks
)
-# #### Further diagnostic plots
-
-if nb:
- import dapper.tools.viz as viz
+# ### Further diagnostic plots
- viz.plot_rank_histogram(xp.stats)
- viz.plot_err_components(xp.stats)
- viz.plot_hovmoller(xx)
+import dapper.tools.viz as viz
-# #### Explore objects
+viz.plot_rank_histogram(xp.stats)
+viz.plot_err_components(xp.stats)
+viz.plot_hovmoller(xx)
-if nb:
- print(xp)
-
-if nb:
- print(HMM)
-
-# #### Excercise: Try out each of the above DA methods (currently commented out).
-# Next, remove the call to `replay`, and set `liveplots=False` above.
-# Now, use the iterative EnKS (`iEnKS`), and try to find a parameter combination
-# for it so that you achieve a lower `rmse.a` than with the `PartFilt`.
+# ### Excercise (methods)
+# - Try out each of the above DA methods (currently commented out).
+# - Next, remove the call to `replay`, and set `liveplots=False` above.
+# - Now, use the iterative EnKS (`iEnKS`), and try to find a parameter combination
+# for it so that you achieve a lower `rmse.a` than with the `PartFilt`.
#
# *Hint*: In general, there is no free lunch. Similarly, not all methods work
# for all problems; additionally, methods often have parameters that require
@@ -81,13 +82,15 @@
# will have to tune it yourself. The example script `basic_2` shows how DAPPER
# facilitates the tuning process, and `basic_3` takes this further.
-# #### Excercise: Run an experiment for each of these models
+# ### Excercise (models)
+# Run an experiment for each of these models
+#
# - LotkaVolterra
# - Lorenz96
# - LA
# - QG
-# #### Excercise: Printing other diagnostics.
+# ### Excercise (diagnostics)
# - Create a new code cell, and copy-paste the above `print(...tabulate)`
# command into it. Then, replace `rmse` by `err.rms`. This should yield
# the same printout, as is merely an abbreviation of the latter.
@@ -98,5 +101,7 @@
# print the regular mean (`.m`) averages. Explain why `err.m` is nearly zero,
# in contrast to `err.rms`.
-# #### Excercise: Why are the replay plots not as smooth as the liveplot?
+# ### Excercise (memory)
+# Why are the replay plots not as smooth as the liveplot?
+#
# *Hint*: provide the keyword `store_u=True` to `assimilate()` to avoid this.
diff --git a/examples/basic_2.ipynb b/docs/examples/basic_2.ipynb
similarity index 56%
rename from examples/basic_2.ipynb
rename to docs/examples/basic_2.ipynb
index ddbac2f1..712c16d5 100644
--- a/examples/basic_2.ipynb
+++ b/docs/examples/basic_2.ipynb
@@ -5,7 +5,7 @@
"id": "cf1f0bba",
"metadata": {},
"source": [
- "## Illustrate usage of DAPPER to benchmark multiple DA methods."
+ "## Multiple synthetic benchmark experiments"
]
},
{
@@ -13,7 +13,7 @@
"id": "e60a67d5",
"metadata": {},
"source": [
- "#### Imports\n",
+ "### Imports\n",
"NB: If you're on Gooble Colab,\n",
"then replace `%matplotlib notebook` below by\\\n",
"`!python -m pip install git+https://github.com/nansencenter/DAPPER.git`\\\n",
@@ -22,7 +22,7 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 1,
"id": "b1c0d97d",
"metadata": {},
"outputs": [],
@@ -39,24 +39,23 @@
"lines_to_next_cell": 2
},
"source": [
- "#### DA method configurations"
+ "### DA method configurations"
]
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 2,
"id": "9f819077",
"metadata": {},
"outputs": [],
"source": [
- "# fmt: off\n",
"from dapper.mods.Lorenz63.sakov2012 import HMM # Expected rmse.a:\n",
"\n",
"xps = dpr.xpList()\n",
"xps += da.Climatology() # 7.6\n",
"xps += da.OptInterp() # 1.25\n",
- "xps += da.Persistence() # 10.7\n",
- "xps += da.PreProg(lambda k, xx, yy: xx[k]) # 0\n",
+ "#xps += da.Persistence() # 10.7\n",
+ "#xps += da.PreProg(lambda k, xx, yy: xx[k]) # 0\n",
"xps += da.Var3D(xB=0.1) # 1.03\n",
"xps += da.ExtKF(infl=90) # 0.87\n",
"xps += da.EnKF('Sqrt' , N=3 , infl=1.30) # 0.82\n",
@@ -67,8 +66,7 @@
"xps += da.PartFilt( N=100 , reg=2.4 , NER=0.3) # 0.38\n",
"xps += da.PartFilt( N=800 , reg=0.9 , NER=0.2) # 0.28\n",
"# xps += da.PartFilt( N=4000, reg=0.7 , NER=0.05) # 0.27\n",
- "# xps += da.PFxN(xN=1000, N=30 , Qs=2 , NER=0.2) # 0.56\n",
- "# # fmt: on"
+ "# xps += da.PFxN(xN=1000, N=30 , Qs=2 , NER=0.2) # 0.56"
]
},
{
@@ -81,12 +79,11 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 3,
"id": "e665575f",
"metadata": {},
"outputs": [],
"source": [
- "# fmt: off\n",
"# from dapper.mods.Lorenz96.sakov2008 import HMM # Expected rmse.a:\n",
"# xps = dpr.xpList()\n",
"# xps += da.Climatology() # 3.6\n",
@@ -101,8 +98,7 @@
"# xps += da.iEnKS('Sqrt' , N=40, infl=1.01, rot=True) # 0.17\n",
"# # With localisation:\n",
"# xps += da.LETKF( N=7 , infl=1.04, rot=True, loc_rad=4) # 0.22\n",
- "# xps += da.SL_EAKF( N=7 , infl=1.07, rot=True, loc_rad=6) # 0.23\n",
- "# fmt: on"
+ "# xps += da.SL_EAKF( N=7 , infl=1.07, rot=True, loc_rad=6) # 0.23"
]
},
{
@@ -115,7 +111,7 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 4,
"id": "7929298c",
"metadata": {},
"outputs": [],
@@ -130,7 +126,7 @@
"id": "d8deb761",
"metadata": {},
"source": [
- "#### Launch"
+ "### Launch"
]
},
{
@@ -144,7 +140,7 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 5,
"id": "8b903ed3",
"metadata": {},
"outputs": [],
@@ -163,7 +159,7 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 6,
"id": "07722c35",
"metadata": {},
"outputs": [],
@@ -181,10 +177,46 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 7,
"id": "b24e7557",
"metadata": {},
- "outputs": [],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Experiment gets stored at /Users/para/data/dpr_data/noname/run_2024-10-31__17-12-44\n"
+ ]
+ },
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "Truth & Obs: 100%|█████████████| 5000/5000 [00:00<00:00, 41765.45it/s]\n",
+ "Climatology: 100%|█████████████| 5000/5000 [00:00<00:00, 58936.74it/s]\n",
+ "Truth & Obs: 100%|█████████████| 5000/5000 [00:00<00:00, 42079.80it/s]\n",
+ "OptInterp: 100%|███████████████| 5000/5000 [00:00<00:00, 25958.39it/s]\n",
+ "Truth & Obs: 100%|█████████████| 5000/5000 [00:00<00:00, 42096.02it/s]\n",
+ "Var3D: 100%|███████████████████| 5000/5000 [00:00<00:00, 26433.50it/s]\n",
+ "Truth & Obs: 100%|█████████████| 5000/5000 [00:00<00:00, 38184.81it/s]\n",
+ "ExtKF infl:90: 100%|███████████| 5000/5000 [00:00<00:00, 19740.78it/s]\n",
+ "Truth & Obs: 100%|█████████████| 5000/5000 [00:00<00:00, 41851.22it/s]\n",
+ "EnKF infl:1.3 upd_a:Sqrt N:3 rot:False: 100%|█| 5000/5000 [00:00<00:00\n",
+ "Truth & Obs: 100%|█████████████| 5000/5000 [00:00<00:00, 40812.37it/s]\n",
+ "EnKF infl:1.02 upd_a:Sqrt N:10 rot:True: 100%|█| 5000/5000 [00:00<00:0\n",
+ "Truth & Obs: 100%|█████████████| 5000/5000 [00:00<00:00, 42888.91it/s]\n",
+ "EnKF infl:0.95 upd_a:PertObs N:500 rot:False: 100%|█| 5000/5000 [00:00\n",
+ "Truth & Obs: 100%|█████████████| 5000/5000 [00:00<00:00, 40611.08it/s]\n",
+ "EnKF_N infl:1 N:10 rot:True xN:1: 100%|█| 5000/5000 [00:00<00:00, 1325\n",
+ "Truth & Obs: 100%|█████████████| 5000/5000 [00:00<00:00, 40498.23it/s]\n",
+ "iEnKS infl:1.02 upd_a:Sqrt N:10 rot:True: 100%|█| 201/201 [00:03<00:00\n",
+ "Truth & Obs: 100%|█████████████| 5000/5000 [00:00<00:00, 42317.38it/s]\n",
+ "PartFilt N:100 reg:2.4 NER:0.3: 100%|█| 5000/5000 [00:00<00:00, 13981.\n",
+ "Truth & Obs: 100%|█████████████| 5000/5000 [00:00<00:00, 42299.37it/s]\n",
+ "PartFilt N:800 reg:0.9 NER:0.2: 100%|█| 5000/5000 [00:00<00:00, 8458.1\n"
+ ]
+ }
+ ],
"source": [
"save_as = xps.launch(HMM, liveplots=False)"
]
@@ -194,17 +226,37 @@
"id": "7e44ffe8",
"metadata": {},
"source": [
- "#### Print results"
+ "### Print results"
]
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 8,
"id": "8bd94fa0",
"metadata": {},
- "outputs": [],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ " da_method infl upd_a N rot xN reg NER | rmse.a 1σ \n",
+ "-- ----------- ----- ------- --- ----- -- --- --- - ------------\n",
+ " 0 Climatology | 7.4 ±0.3 \n",
+ " 1 OptInterp | 1.23 ±0.04\n",
+ " 2 Var3D | 1.07 ±0.06\n",
+ " 3 ExtKF 90 | 0.88 ±0.09\n",
+ " 4 EnKF 1.3 Sqrt 3 False | 0.8 ±0.1 \n",
+ " 5 EnKF 1.02 Sqrt 10 True | 0.59 ±0.06\n",
+ " 6 EnKF 0.95 PertObs 500 False | 1.1 ±0.3 \n",
+ " 7 EnKF_N 1 10 True 1 | 0.71 ±0.07\n",
+ " 8 iEnKS 1.02 Sqrt 10 True | 0.23 ±0.02\n",
+ " 9 PartFilt 100 2.4 0.3 | 0.32 ±0.03\n",
+ "10 PartFilt 800 0.9 0.2 | 0.20 ±0.03\n"
+ ]
+ }
+ ],
"source": [
- "print(xps.tabulate_avrgs())"
+ "print(xps.tabulate_avrgs(statkeys=[\"rmse.a\"]))"
]
}
],
@@ -230,7 +282,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.9.16"
+ "version": "3.12.4"
}
},
"nbformat": 4,
diff --git a/examples/basic_2.py b/docs/examples/basic_2.py
similarity index 88%
rename from examples/basic_2.py
rename to docs/examples/basic_2.py
index 65a4e2d3..48bc80d3 100644
--- a/examples/basic_2.py
+++ b/docs/examples/basic_2.py
@@ -1,6 +1,6 @@
-# ## Illustrate usage of DAPPER to benchmark multiple DA methods.
+# ## Multiple synthetic benchmark experiments
-# #### Imports
+# ### Imports
# NB: If you're on Gooble Colab,
# then replace `%matplotlib notebook` below by\
# `!python -m pip install git+https://github.com/nansencenter/DAPPER.git`\
@@ -10,18 +10,17 @@
import dapper as dpr # noqa: I001
import dapper.da_methods as da
-# #### DA method configurations
+# ### DA method configurations
# +
-# fmt: off
from dapper.mods.Lorenz63.sakov2012 import HMM # Expected rmse.a:
xps = dpr.xpList()
xps += da.Climatology() # 7.6
xps += da.OptInterp() # 1.25
-xps += da.Persistence() # 10.7
-xps += da.PreProg(lambda k, xx, yy: xx[k]) # 0
+#xps += da.Persistence() # 10.7
+#xps += da.PreProg(lambda k, xx, yy: xx[k]) # 0
xps += da.Var3D(xB=0.1) # 1.03
xps += da.ExtKF(infl=90) # 0.87
xps += da.EnKF('Sqrt' , N=3 , infl=1.30) # 0.82
@@ -33,13 +32,11 @@
xps += da.PartFilt( N=800 , reg=0.9 , NER=0.2) # 0.28
# xps += da.PartFilt( N=4000, reg=0.7 , NER=0.05) # 0.27
# xps += da.PFxN(xN=1000, N=30 , Qs=2 , NER=0.2) # 0.56
-# # fmt: on
# -
# #### With Lorenz-96 instead
# +
-# fmt: off
# from dapper.mods.Lorenz96.sakov2008 import HMM # Expected rmse.a:
# xps = dpr.xpList()
# xps += da.Climatology() # 3.6
@@ -55,7 +52,6 @@
# # With localisation:
# xps += da.LETKF( N=7 , infl=1.04, rot=True, loc_rad=4) # 0.22
# xps += da.SL_EAKF( N=7 , infl=1.07, rot=True, loc_rad=6) # 0.23
-# fmt: on
# -
# #### Other models (suitable xp's listed in HMM files):
@@ -66,7 +62,7 @@
# from dapper.mods.LotkaVolterra.settings101 import HMM
# -
-# #### Launch
+# ### Launch
# Write some more non-arg parameters to the `xps`. In this case we set the seed,
# so that repeat experiments produce exactly the same result.
@@ -82,6 +78,6 @@
save_as = xps.launch(HMM, liveplots=False)
-# #### Print results
+# ### Print results
-print(xps.tabulate_avrgs())
+print(xps.tabulate_avrgs(statkeys=["rmse.a"]))
diff --git a/examples/basic_3a.py b/docs/examples/basic_3a.py
similarity index 100%
rename from examples/basic_3a.py
rename to docs/examples/basic_3a.py
diff --git a/examples/basic_3b.py b/docs/examples/basic_3b.py
similarity index 100%
rename from examples/basic_3b.py
rename to docs/examples/basic_3b.py
diff --git a/examples/param_estim.py b/docs/examples/param_estim.py
similarity index 97%
rename from examples/param_estim.py
rename to docs/examples/param_estim.py
index 85184e73..cfc3845a 100644
--- a/examples/param_estim.py
+++ b/docs/examples/param_estim.py
@@ -6,10 +6,10 @@
# (concatenating) the state vector with the parameter (vector).
#
# This particular experiment is a reproduction of section 3.3 of
-# `bib.bocquet2013joint`. Briefly: the unknown parameter is the forcing used in
+# [bocquet2013joint][]. Briefly: the unknown parameter is the forcing used in
# Lorenz-96; only the state is observed; both parameter and state get estimated.
#
-# This example builds mostly on `examples/basic_2.py`. For brevity, it does not
+# This example builds mostly on `docs/examples/basic_2.py`. For brevity, it does not
# contain some of the facilities of `examples/basic_3.py` to run and analyse a
# larger numbers of experiments.
#
diff --git a/examples/stoch_model1.py b/docs/examples/stoch_model1.py
similarity index 96%
rename from examples/stoch_model1.py
rename to docs/examples/stoch_model1.py
index 5ff1c7ce..95156a78 100644
--- a/examples/stoch_model1.py
+++ b/docs/examples/stoch_model1.py
@@ -1,4 +1,4 @@
-# ## Reproduce basics of `bib.grudzien2020numerical`, and do live plotting.
+# ## Reproduce basics of [grudzien2020numerical][], and do live plotting.
# This is a simple demonstration of the effect of using low-precision numerics
# for a perfect-random model configuration in which the system is not dominated
# by the noise. In this case, we see a failure of the ensemble generated
diff --git a/examples/stoch_models.py b/docs/examples/stoch_models.py
similarity index 100%
rename from examples/stoch_models.py
rename to docs/examples/stoch_models.py
diff --git a/examples/time-dep-obs-operator.py b/docs/examples/time-dep-obs-operator.py
similarity index 100%
rename from examples/time-dep-obs-operator.py
rename to docs/examples/time-dep-obs-operator.py
diff --git a/docs/gen_ref_pages.py b/docs/gen_ref_pages.py
new file mode 100644
index 00000000..509c3975
--- /dev/null
+++ b/docs/gen_ref_pages.py
@@ -0,0 +1,41 @@
+"""Generate the code reference pages."""
+# Based on https://mkdocstrings.github.io/recipes/
+
+from pathlib import Path
+
+import mkdocs_gen_files
+
+nav = mkdocs_gen_files.Nav()
+
+root = Path(__file__).parent.parent
+src = root / "dapper"
+
+for path in sorted(src.rglob("*.py")):
+ module_path = path.relative_to(src).with_suffix("")
+ doc_path = path.relative_to(src).with_suffix(".md")
+ full_doc_path = Path("reference", doc_path)
+
+ parts = tuple(module_path.parts)
+
+ if parts[-1] == "__init__":
+ parts = parts[:-1] or src.parts[-1:]
+ if not parts:
+ # we're in root pkg
+ parts = src.parts[-1:]
+ doc_path = doc_path.with_name("index.md")
+ full_doc_path = full_doc_path.with_name("index.md")
+ elif parts[-1] == "__main__":
+ continue
+
+ # PS: rm mkdocs_gen_files to get to inspect actual .md files
+ # NB: will (over)write in docs/ folder.
+ with mkdocs_gen_files.open(full_doc_path, "w") as fd:
+ identifier = ".".join(parts)
+ print("::: " + identifier, file=fd)
+
+ mkdocs_gen_files.set_edit_path(full_doc_path, path.relative_to(root))
+
+# > So basically, you can use the literate-nav plugin just for its ability to
+# > infer only sub-directories, without ever writing any actual "literate navs".
+# with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as nav_file:
+# nav_file.writelines(nav.build_literate_nav())
diff --git a/docs/imgs/ex1.jpg b/docs/images/ex1.jpg
similarity index 100%
rename from docs/imgs/ex1.jpg
rename to docs/images/ex1.jpg
diff --git a/docs/imgs/ex3.svg b/docs/images/ex3.svg
similarity index 100%
rename from docs/imgs/ex3.svg
rename to docs/images/ex3.svg
diff --git a/docs/imgs/CW3E-Logo-Horizontal-FullColor.png b/docs/images/logos/CW3E-Logo-Horizontal-FullColor.png
similarity index 100%
rename from docs/imgs/CW3E-Logo-Horizontal-FullColor.png
rename to docs/images/logos/CW3E-Logo-Horizontal-FullColor.png
diff --git a/docs/imgs/UoR-logo.png b/docs/images/logos/UoR-logo.png
similarity index 100%
rename from docs/imgs/UoR-logo.png
rename to docs/images/logos/UoR-logo.png
diff --git a/docs/imgs/logo.png b/docs/images/logos/logo.png
similarity index 100%
rename from docs/imgs/logo.png
rename to docs/images/logos/logo.png
diff --git a/docs/imgs/logo_wtxt.png b/docs/images/logos/logo_wtxt.png
similarity index 100%
rename from docs/imgs/logo_wtxt.png
rename to docs/images/logos/logo_wtxt.png
diff --git a/docs/imgs/nansen-logo.png b/docs/images/logos/nansen-logo.png
similarity index 100%
rename from docs/imgs/nansen-logo.png
rename to docs/images/logos/nansen-logo.png
diff --git a/docs/imgs/nceologo1000.png b/docs/images/logos/nceologo1000.png
similarity index 100%
rename from docs/imgs/nceologo1000.png
rename to docs/images/logos/nceologo1000.png
diff --git a/docs/imgs/norce-logo.png b/docs/images/logos/norce-logo.png
similarity index 100%
rename from docs/imgs/norce-logo.png
rename to docs/images/logos/norce-logo.png
diff --git a/docs/snippets/ex_resample.py b/docs/images/snippets/ex_resample.py
similarity index 100%
rename from docs/snippets/ex_resample.py
rename to docs/images/snippets/ex_resample.py
diff --git a/docs/snippets/iEnKS_Ea.jpg b/docs/images/snippets/iEnKS_Ea.jpg
similarity index 100%
rename from docs/snippets/iEnKS_Ea.jpg
rename to docs/images/snippets/iEnKS_Ea.jpg
diff --git a/docs/snippets/serial_sqrt.jpg b/docs/images/snippets/serial_sqrt.jpg
similarity index 100%
rename from docs/snippets/serial_sqrt.jpg
rename to docs/images/snippets/serial_sqrt.jpg
diff --git a/docs/snippets/sigmoid.jpg b/docs/images/snippets/sigmoid.jpg
similarity index 100%
rename from docs/snippets/sigmoid.jpg
rename to docs/images/snippets/sigmoid.jpg
diff --git a/docs/snippets/trHK.jpg b/docs/images/snippets/trHK.jpg
similarity index 100%
rename from docs/snippets/trHK.jpg
rename to docs/images/snippets/trHK.jpg
diff --git a/docs/snippets/unbiased_skew_kurt.jpg b/docs/images/snippets/unbiased_skew_kurt.jpg
similarity index 100%
rename from docs/snippets/unbiased_skew_kurt.jpg
rename to docs/images/snippets/unbiased_skew_kurt.jpg
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 00000000..0dad5287
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,6 @@
+---
+hide:
+- navigation
+---
+
+--8<-- "README.md"
diff --git a/docs/javascripts/extra.js b/docs/javascripts/extra.js
new file mode 100644
index 00000000..584b3c70
--- /dev/null
+++ b/docs/javascripts/extra.js
@@ -0,0 +1,9 @@
+// https://squidfunk.github.io/mkdocs-material/customization/#additional-javascript
+console.log("This message appears on every page")
+
+// Tag converted notebooks (to facilitate custom styling)
+document.addEventListener("DOMContentLoaded", function() {
+ if (document.querySelector('.jp-Notebook')) {
+ document.body.classList.add('document-is-notebook');
+ }
+});
diff --git a/docs/javascripts/mathjax.js b/docs/javascripts/mathjax.js
new file mode 100644
index 00000000..0be88e04
--- /dev/null
+++ b/docs/javascripts/mathjax.js
@@ -0,0 +1,19 @@
+window.MathJax = {
+ tex: {
+ inlineMath: [["\\(", "\\)"]],
+ displayMath: [["\\[", "\\]"]],
+ processEscapes: true,
+ processEnvironments: true
+ },
+ options: {
+ ignoreHtmlClass: ".*|",
+ processHtmlClass: "arithmatex"
+ }
+};
+
+document$.subscribe(() => {
+ MathJax.startup.output.clearCache()
+ MathJax.typesetClear()
+ MathJax.texReset()
+ MathJax.typesetPromise()
+})
diff --git a/docs/overrides/main.html b/docs/overrides/main.html
new file mode 100644
index 00000000..67930cce
--- /dev/null
+++ b/docs/overrides/main.html
@@ -0,0 +1,20 @@
+
+{% extends "base.html" %}
+
+{% block extrahead %}
+
+
+{% endblock %}
+
+{% block content %}
+ {{ super() }}
+
+ {#
+ Hello World!
+ {{ page.url }}
+ {{ "relative/link" | url }}
+ {{ "/absolute/link" | url }}
+ {{ "https://www.google.com/" | url }}
+ #}
+
+{% endblock %}
diff --git a/docs/references.md b/docs/references.md
new file mode 100644
index 00000000..caccdcdf
--- /dev/null
+++ b/docs/references.md
@@ -0,0 +1,315 @@
+### `raanes2016thesis`
+
+Patrick N\. Raanes\.
+*Improvements to Ensemble Methods for Data Assimilation in the Geosciences*\.
+PhD thesis, University of Oxford, January 2016\.
+\\url https://ora\.ox\.ac\.uk/objects/uuid:9f9961f0\-6906\-4147\-a8a9\-ca9f2d0e4a12\.
+
+### `bocquet2010beyond`
+
+Marc Bocquet, Carlos A\. Pires, and Lin Wu\.
+`"Beyond Gaussian statistical modeling in geophysical data assimilation"`\.
+*Monthly Weather Review*, 138\(8\):2997–3023, 2010\.
+
+### `bocquet2015expanding`
+
+Marc Bocquet, Patrick N\. Raanes, and Alexis Hannart\.
+`"Expanding the validity of the ensemble Kalman filter without the intrinsic need for inflation"`\.
+*Nonlinear Processes in Geophysics*, 22\(6\):645–662, 2015\.
+
+### `sakov2008implications`
+
+Pavel Sakov and Peter R\. Oke\.
+`"Implications of the form of the ensemble transformation in the ensemble square root filters"`\.
+*Monthly Weather Review*, 136\(3\):1042–1053, 2008\.
+
+### `wiljes2016second`
+
+Jana de Wiljes, Walter Acevedo, and Sebastian Reich\.
+`"Second\-order accurate ensemble transform particle filters"`\.
+*arXiv preprint arXiv:1608\.08179*, 2016\.
+
+### `bocquet2011ensemble`
+
+Marc Bocquet\.
+`"Ensemble Kalman filtering without the intrinsic need for inflation"`\.
+*Nonlinear Processes in Geophysics*, 18\(5\):735–750, 2011\.
+
+### `hoteit2015mitigating`
+
+I\. Hoteit, D\.\-T\. Pham, M\. E\. Gharamti, and X\. Luo\.
+`"Mitigating observation perturbation sampling errors in the stochastic EnKF"`\.
+*Monthly Weather Review*, 143\(7\):2918–2936, 2015\.
+
+### `bocquet2013joint`
+
+Marc Bocquet and Pavel Sakov\.
+`"Joint state and parameter estimation with an iterative ensemble Kalman smoother"`\.
+*Nonlinear Processes in Geophysics*, 20\(5\):803–818, 2013\.
+
+### `hunt2007efficient`
+
+Brian R\. Hunt, Eric J\. Kostelich, and Istvan Szunyogh\.
+`"Efficient data assimilation for spatiotemporal chaos: a local ensemble transform Kalman filter"`\.
+*Physica D: Nonlinear Phenomena*, 230\(1\):112–126, 2007\.
+
+### `karspeck2007experimental`
+
+Alicia R\. Karspeck and Jeffrey L\. Anderson\.
+`"Experimental implementation of an ensemble adjustment filter for an intermediate ENSO model"`\.
+*Journal of Climate*, 20\(18\):4638–4658, 2007\.
+
+### `todter2015second`
+
+Julian Tödter and Bodo Ahrens\.
+`"A second\-order exact ensemble square root filter for nonlinear data assimilation"`\.
+*Monthly Weather Review*, 143\(4\):1347–1367, 2015\.
+
+### `chen2003bayesian`
+
+Zhe Chen\.
+`"Bayesian filtering: from Kalman filters to particle filters, and beyond"`\.
+*Statistics*, 182\(1\):1–69, 2003\.
+
+### `bocquet2012combining`
+
+Marc Bocquet and Pavel Sakov\.
+`"Combining inflation\-free and iterative ensemble Kalman filters for strongly nonlinear systems"`\.
+*Nonlinear Processes in Geophysics*, 19\(3\):383–399, 2012\.
+
+### `doucet2001sequential`
+
+Arnaud Doucet, Nando De Freitas, and Neil Gordon\.
+*Sequential Monte Carlo Methods in Practice*\.
+Springer, 2001\.
+
+### `liu2001theoretical`
+
+Jun S\. Liu, Rong Chen, and Tanya Logvinenko\.
+`"A theoretical framework for sequential importance sampling with resampling"`\.
+In *Sequential Monte Carlo methods in practice*, pages 225–246\.
+Springer, 2001\.
+
+### `bocquet2014iterative`
+
+Marc Bocquet and Pavel Sakov\.
+`"An iterative ensemble Kalman smoother"`\.
+*Quarterly Journal of the Royal Meteorological Society*, 140\(682\):1521–1535, 2014\.
+
+### `sakov2008deterministic`
+
+Pavel Sakov and Peter R\. Oke\.
+`"A deterministic formulation of the ensemble Kalman filter: an alternative to ensemble square root filters"`\.
+*Tellus A*, 60\(2\):361–371, 2008\.
+
+### `zupanski2005maximum`
+
+Milija Zupanski\.
+`"Maximum likelihood ensemble filter: theoretical aspects"`\.
+*Monthly Weather Review*, 133\(6\):1710–1726, 2005\.
+
+### `sakov2012iterative`
+
+Pavel Sakov, Dean S\. Oliver, and Laurent Bertino\.
+`"An iterative EnKF for strongly nonlinear systems\."`\.
+*Monthly Weather Review*, 140\(6\):1988–2004, 2012\.
+
+### `evensen2009ensemble`
+
+G\. Evensen\.
+`"The ensemble Kalman filter for combined state and parameter estimation"`\.
+*Control Systems, IEEE*, 29\(3\):83–104, 2009\.
+
+### `raanes2015rts`
+
+Patrick Nima Raanes\.
+`"On the ensemble Rauch\-Tung\-Striebel smoother and its equivalence to the ensemble Kalman smoother"`\.
+*Quarterly Journal of the Royal Meteorological Society*, 142\(696\):1259–1264, 2016\.
+
+### `anderson2010non`
+
+Jeffrey L\. Anderson\.
+`"A non\-Gaussian ensemble filter update for data assimilation\."`\.
+*Monthly Weather Review*, 138\(11\):4186–4198, 2010\.
+
+### `raanes2019revising`
+
+Patrick Nima Raanes, Andreas Størksen Stordal, and Geir Evensen\.
+`"Revising the stochastic iterative ensemble smoother"`\.
+*Nonlinear Processes in Geophysics*, 26\(3\):325–338, 2019\.
+
+### `van2009particle`
+
+Peter Jan van Leeuwen\.
+`"Particle filtering in geophysical systems"`\.
+*Monthly Weather Review*, 137\(12\):4089–4114, 2009\.
+
+### `raanes2019adaptive`
+
+Patrick N\. Raanes, Marc Bocquet, and Alberto Carrassi\.
+`"Adaptive covariance inflation in the ensemble Kalman filter by Gaussian scale mixtures"`\.
+*Quarterly Journal of the Royal Meteorological Society*, 145\(718\):53–75, 2019\.
+[doi:10\.1002/qj\.3386](https://doi.org/10.1002/qj.3386)\.
+
+### `lei2011moment`
+
+Jing Lei and Peter Bickel\.
+`"A moment matching ensemble filter for nonlinear non\-Gaussian data assimilation"`\.
+*Monthly Weather Review*, 139\(12\):3964–3973, 2011\.
+
+### `wikle2007bayesian`
+
+C\. K\. Wikle and L\. M\. Berliner\.
+`"A Bayesian tutorial for data assimilation"`\.
+*Physica D: Nonlinear Phenomena*, 230\(1\-2\):1–16, 2007\.
+
+### `doucet2009tutorial`
+
+Arnaud Doucet and Adam M\. Johansen\.
+`"A tutorial on particle filtering and smoothing: fifteen years later"`\.
+*Handbook of Nonlinear Filtering*, pages 656–704, 2009\.
+
+### `bocquet2016localization`
+
+Marc Bocquet\.
+`"Localization and the iterative ensemble Kalman smoother"`\.
+*Quarterly Journal of the Royal Meteorological Society*, 142\(695\):1075–1089, 2016\.
+
+### `raanes2014ext`
+
+Patrick N\. Raanes, Alberto Carrassi, and Laurent Bertino\.
+`"Extending the square root method to account for model noise in the ensemble Kalman filter"`\.
+*Monthly Weather Review*, 143\(10\):3857–3873, 2015\.
+
+### `lorenz1998optimal`
+
+Edward N\. Lorenz and Kerry A\. Emanuel\.
+`"Optimal sites for supplementary weather observations: simulation with a small model"`\.
+*Journal of the Atmospheric Sciences*, 55\(3\):399–414, 1998\.
+
+### `frei2013mixture`
+
+Marco Frei and Hans R Künsch\.
+`"Mixture ensemble kalman filters"`\.
+*Computational Statistics & Data Analysis*, 58:127–138, 2013\.
+
+### `frei2013bridging`
+
+Marco Frei and Hans R Künsch\.
+`"Bridging the ensemble Kalman and particle filters"`\.
+*Biometrika*, pages ast020, 2013\.
+
+### `miyoshi2011gaussian`
+
+Takemasa Miyoshi\.
+`"The Gaussian approach to adaptive covariance inflation and its implementation with the local ensemble transform Kalman filter"`\.
+*Monthly Weather Review*, 139\(5\):1519–1535, 2011\.
+
+### `mandel2016hybrid`
+
+J\. Mandel, E\. Bergou, S\. Gürol, S\. Gratton, and I\. Kasanický\.
+`"Hybrid Levenberg\-Marquardt and weak\-constraint ensemble Kalman smoother method"`\.
+*Nonlinear Processes in Geophysics*, 23\(2\):59–73, 2016\.
+
+### `anderson2009spatially`
+
+Jeffrey L\. Anderson\.
+`"Spatially and temporally varying adaptive covariance inflation for ensemble filters"`\.
+*Tellus A*, 61\(1\):72–83, 2009\.
+
+### `bengtsson2003toward`
+
+Thomas Bengtsson, Chris Snyder, and Doug Nychka\.
+`"Toward a nonlinear ensemble filter for high\-dimensional systems"`\.
+*Journal of Geophysical Research: Atmospheres*, 2003\.
+
+### `lorenz1996predictability`
+
+Edward N\. Lorenz\.
+`"Predictability: a problem partly solved"`\.
+In *Proc\. ECMWF Seminar on Predictability*, volume 1, 1–18\. Reading, UK, 1996\.
+
+### `wilks2005effects`
+
+Daniel S\. Wilks\.
+`"Effects of stochastic parametrizations in the Lorenz'96 system"`\.
+*Quarterly Journal of the Royal Meteorological Society*, 131\(606\):389–407, 2005\.
+
+### `harty2021eigenvector`
+
+Travis Harty, Matthias Morzfeld, and Chris Snyder\.
+`"Eigenvector\-spatial localisation"`\.
+*Tellus A: Dynamic Meteorology and Oceanography*, 73\(1\):1–18, 2021\.
+
+### `rainwater2013mixed`
+
+Sabrina Rainwater and Brian Hunt\.
+`"Mixed\-resolution ensemble data assimilation"`\.
+*Monthly weather review*, 141\(9\):3007–3021, 2013\.
+
+### `counillon2009application`
+
+François Counillon, Pavel Sakov, and Laurent Bertino\.
+`"Application of a hybrid \`enkf\-oi\` to ocean forecasting"`\.
+*Ocean Science*, 5\(4\):389–401, 2009\.
+
+### `pajonk2012deterministic`
+
+Oliver Pajonk, Bojana V\. Rosić, Alexander Litvinenko, and Hermann G\. Matthies\.
+`"A deterministic filter for non\-Gaussian Bayesian estimation—applications to dynamical system estimation with noisy measurements"`\.
+*Physica D: Nonlinear Phenomena*, 241\(7\):775–788, 2012\.
+
+### `lorenz2005look`
+
+Edward N\. Lorenz\.
+`"A look at some details of the growth of initial uncertainties"`\.
+*Tellus A: Dynamic Meteorology and Oceanography*, 57\(1\):1–11, 2005\.
+
+### `bocquet2019consistency`
+
+Marc Bocquet and Alban Farchi\.
+`"On the consistency of the local ensemble square root kalman filter perturbation update"`\.
+*Tellus A: Dynamic Meteorology and Oceanography*, 71\(1\):1613142, 2019\.
+
+### `lorenz1984irregularity`
+
+Edward N\. Lorenz\.
+`"Irregularity: a fundamental property of the atmosphere"`\.
+*Tellus A*, 36\(2\):98–110, 1984\.
+
+### `vano2006chaos`
+
+J\. A\. Vano, J\. C\. Wildenberg, M\. B\. Anderson, J\. K\. Noel, and J\. C\. Sprott\.
+`"Chaos in low\-dimensional Lotka–Volterra models of competition"`\.
+*Nonlinearity*, 19\(10\):2391, 2006\.
+
+### `emerick2012history`
+
+Alexandre A\. Emerick and Albert C\. Reynolds\.
+`"History matching time\-lapse seismic data using the ensemble Kalman filter with multiple data assimilations"`\.
+*Computational Geosciences*, 16\(3\):639–659, 2012\.
+
+### `pinheiro2019efficient`
+
+Flavia R\. Pinheiro, Peter J\. van Leeuwen, and Gernot Geppert\.
+`"Efficient nonlinear data assimilation using synchronization in a particle filter"`\.
+*Quarterly Journal of the Royal Meteorological Society*, 145\(723\):2510–2523, 2019\.
+
+### `vissio2020mechanics`
+
+Gabriele Vissio and Valerio Lucarini\.
+`"Mechanics and thermodynamics of a new minimal model of the atmosphere"`\.
+*The European Physical Journal Plus*, 135\(10\):1–21, 2020\.
+
+### `grudzien2020numerical`
+
+Colin Grudzien, Marc Bocquet, and Alberto Carrassi\.
+`"On the numerical integration of the lorenz\-96 model, with scalar additive noise, for benchmark twin experiments"`\.
+*Geoscientific Model Development*, 13\(4\):1903–1924, 2020\.
+
+### `lorenz2005designing`
+
+Edward\. N\. Lorenz\.
+`"Designing chaotic models"`\.
+*Journal of the Atmospheric Sciences*, 62\(5\):1574–1587, 2005\.
+
diff --git a/docs/snippets/README.md b/docs/snippets/README.md
deleted file mode 100644
index 72ee4691..00000000
--- a/docs/snippets/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-This folder does not provide *the* DAPPER documentation,
-but merely some snippets referred to at various places in the code.
-See the [Getting started in the README](https://github.com/nansencenter/DAPPER#getting-started) for more documentation.
diff --git a/docs/stylesheets/code_select.css b/docs/stylesheets/code_select.css
new file mode 100644
index 00000000..20885379
--- /dev/null
+++ b/docs/stylesheets/code_select.css
@@ -0,0 +1,3 @@
+.language-pycon .gp, .language-pycon .go { /* Generic.Prompt, Generic.Output */
+ user-select: none;
+}
diff --git a/docs/stylesheets/jupyter.css b/docs/stylesheets/jupyter.css
new file mode 100644
index 00000000..880a9921
--- /dev/null
+++ b/docs/stylesheets/jupyter.css
@@ -0,0 +1,8 @@
+/* No need for these margins in the rendered jupyter notebooks */
+.document-is-notebook h1 { margin-bottom: 0; }
+.jp-Notebook h2:first-of-type { margin-top: 0; }
+
+/* stderr is used by tqdm progressbar so should not be red */
+.jupyter-wrapper .jp-RenderedText[data-mime-type="application/vnd.jupyter.stderr"] {
+ background-color: #cdd6d6ff !important;
+}
diff --git a/docs/templates/custom.css b/docs/templates/custom.css
deleted file mode 100644
index 712a3a18..00000000
--- a/docs/templates/custom.css
+++ /dev/null
@@ -1,4 +0,0 @@
-nav.pdoc {
- background-color: white;
- box-shadow: 0 0 20px rgb(50 50 50 / 20%);
-}
diff --git a/docs/templates/frame.html.jinja2 b/docs/templates/frame.html.jinja2
deleted file mode 100644
index 48fa0d93..00000000
--- a/docs/templates/frame.html.jinja2
+++ /dev/null
@@ -1,5 +0,0 @@
-{% extends "default/frame.html.jinja2" %}
-{% block head %}
-
-
-{% endblock %}
diff --git a/docs/templates/index.html.jinja2 b/docs/templates/index.html.jinja2
deleted file mode 100644
index 7f670a9b..00000000
--- a/docs/templates/index.html.jinja2
+++ /dev/null
@@ -1,2 +0,0 @@
-{% set root_module_name = 'dapper' %}
-{% extends "default/index.html.jinja2" %}
diff --git a/docs/templates/module.html.jinja2 b/docs/templates/module.html.jinja2
deleted file mode 100644
index faa4e616..00000000
--- a/docs/templates/module.html.jinja2
+++ /dev/null
@@ -1,19 +0,0 @@
-{% set logo = 'https://raw.githubusercontent.com/nansencenter/DAPPER/master/docs/imgs/logo_wtxt.png' %}
-{% set logo_link = '/' %}
-{% set favicon = 'https://raw.githubusercontent.com/nansencenter/DAPPER/master/docs/imgs/logo.png' %}
-
-{% extends "default/module.html.jinja2" %}
-
-{% block title %}DAPPER docs: {{ module.modulename }}{% endblock %}
-
-{# Rm "Module Index" button on root package #}
-{% block module_list_link %}
- {% set parentmodule = ".".join(module.modulename.split(".")[:-1]) %}
- {% if parentmodule and parentmodule in all_modules %}
-
- {% include "resources/box-arrow-in-left.svg" %}
-
- {{- parentmodule -}}
-
- {% endif %}
-{% endblock %}
diff --git a/examples/basic_1.ipynb b/examples/basic_1.ipynb
deleted file mode 100644
index b7326914..00000000
--- a/examples/basic_1.ipynb
+++ /dev/null
@@ -1,328 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "id": "3c5106d3",
- "metadata": {},
- "source": [
- "## Illustrate usage of DAPPER to (interactively) run a synthetic (\"twin\") experiment."
- ]
- },
- {
- "cell_type": "markdown",
- "id": "fe5f6628",
- "metadata": {},
- "source": [
- "#### Imports\n",
- "NB: On Gooble Colab,\n",
- "*replace* `%matplotlib notebook` (right below) by\\\n",
- "`!python -m pip install git+https://github.com/nansencenter/DAPPER.git`"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "89f5e089",
- "metadata": {},
- "outputs": [],
- "source": [
- "%matplotlib notebook\n",
- "from mpl_tools import is_notebook_or_qt as nb"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "fc681615",
- "metadata": {},
- "outputs": [],
- "source": [
- "import dapper as dpr\n",
- "import dapper.da_methods as da"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "8b272589",
- "metadata": {},
- "outputs": [],
- "source": [
- "# #### Load experiment setup: the hidden Markov model (HMM)\n",
- "from dapper.mods.Lorenz63.sakov2012 import HMM"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "7afbdeb6",
- "metadata": {},
- "source": [
- "#### Generate the same random numbers each time this script is run"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "0e74274d",
- "metadata": {},
- "outputs": [],
- "source": [
- "dpr.set_seed(3000)"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "2ce9b72c",
- "metadata": {},
- "source": [
- "#### Simulate synthetic truth (xx) and noisy obs (yy)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "c54bcca2",
- "metadata": {},
- "outputs": [],
- "source": [
- "HMM.tseq.T = 30 # shorten experiment\n",
- "xx, yy = HMM.simulate()"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "3c40799e",
- "metadata": {},
- "source": [
- "#### Specify a DA method configuration (\"xp\" is short for \"experiment\")"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "b39ebe02",
- "metadata": {},
- "outputs": [],
- "source": [
- "# xp = da.OptInterp()\n",
- "# xp = da.Var3D()\n",
- "# xp = da.ExtKF(infl=90)\n",
- "xp = da.EnKF(\"Sqrt\", N=10, infl=1.02, rot=True)\n",
- "# xp = da.PartFilt(N=100, reg=2.4, NER=0.3)"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "08f20322",
- "metadata": {},
- "source": [
- "#### Assimilate yy, knowing the HMM; xx is used to assess the performance"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "0a0ea6ff",
- "metadata": {},
- "outputs": [],
- "source": [
- "xp.assimilate(HMM, xx, yy, liveplots=not nb)"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "18ff2a15",
- "metadata": {},
- "source": [
- "#### Average the time series of various statistics"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "a7f6fb40",
- "metadata": {},
- "outputs": [],
- "source": [
- "# print(xp.stats) # ⇒ long printout\n",
- "xp.stats.average_in_time()"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "842ad66b",
- "metadata": {},
- "source": [
- "#### Print some of these time-averages"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "cc4e9955",
- "metadata": {},
- "outputs": [],
- "source": [
- "# print(xp.avrgs) # ⇒ long printout\n",
- "print(xp.avrgs.tabulate([\"rmse.a\", \"rmv.a\"]))"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "f130c09b",
- "metadata": {},
- "source": [
- "#### Replay liveplotters"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "a7ebf5e6",
- "metadata": {},
- "outputs": [],
- "source": [
- "xp.stats.replay(\n",
- " # speed=.6 # `speed` does not work in notebooks\n",
- ")"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "6335abba",
- "metadata": {},
- "source": [
- "#### Further diagnostic plots"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "450a7fc1",
- "metadata": {},
- "outputs": [],
- "source": [
- "if nb:\n",
- " import dapper.tools.viz as viz\n",
- "\n",
- " viz.plot_rank_histogram(xp.stats)\n",
- " viz.plot_err_components(xp.stats)\n",
- " viz.plot_hovmoller(xx)"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "51a3c22f",
- "metadata": {},
- "source": [
- "#### Explore objects"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "70fa4918",
- "metadata": {},
- "outputs": [],
- "source": [
- "if nb:\n",
- " print(xp)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "3274e3b1",
- "metadata": {},
- "outputs": [],
- "source": [
- "if nb:\n",
- " print(HMM)"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "304273d1",
- "metadata": {},
- "source": [
- "#### Excercise: Try out each of the above DA methods (currently commented out).\n",
- "Next, remove the call to `replay`, and set `liveplots=False` above.\n",
- "Now, use the iterative EnKS (`iEnKS`), and try to find a parameter combination\n",
- "for it so that you achieve a lower `rmse.a` than with the `PartFilt`.\n",
- "\n",
- "*Hint*: In general, there is no free lunch. Similarly, not all methods work\n",
- "for all problems; additionally, methods often have parameters that require\n",
- "tuning. Luckily, in DAPPER, you should be able to find suitably tuned\n",
- "configuration settings for various DA methods *in the files that define the\n",
- "HMM*. If you do not find a suggested configuration for a given method, you\n",
- "will have to tune it yourself. The example script `basic_2` shows how DAPPER\n",
- "facilitates the tuning process, and `basic_3` takes this further."
- ]
- },
- {
- "cell_type": "markdown",
- "id": "de2e51e4",
- "metadata": {},
- "source": [
- "#### Excercise: Run an experiment for each of these models\n",
- "- LotkaVolterra\n",
- "- Lorenz96\n",
- "- LA\n",
- "- QG"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "bee8dfc6",
- "metadata": {},
- "source": [
- "#### Excercise: Printing other diagnostics.\n",
- "- Create a new code cell, and copy-paste the above `print(...tabulate)`\n",
- " command into it. Then, replace `rmse` by `err.rms`. This should yield\n",
- " the same printout, as is merely an abbreviation of the latter.\n",
- "- Next, figure out how to print the time average *forecast (i.e. prior)* error\n",
- " (and `rmv`) instead. Explain (in broad terms) why the values are larger than\n",
- " for the *analysis* values.\n",
- "- Finally, instead of the `rms` spatial/field averages,\n",
- " print the regular mean (`.m`) averages. Explain why `err.m` is nearly zero,\n",
- " in contrast to `err.rms`."
- ]
- },
- {
- "cell_type": "markdown",
- "id": "ad85889d",
- "metadata": {},
- "source": [
- "#### Excercise: Why are the replay plots not as smooth as the liveplot?\n",
- "*Hint*: provide the keyword `store_u=True` to `assimilate()` to avoid this."
- ]
- }
- ],
- "metadata": {
- "jupytext": {
- "cell_metadata_filter": "-all",
- "formats": "py:light,ipynb",
- "main_language": "python",
- "notebook_metadata_filter": "-all"
- },
- "kernelspec": {
- "display_name": "Python 3 (ipykernel)",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.9.16"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 5
-}
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 00000000..4a691abb
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,191 @@
+# ╔═══════╗
+# ║ Notes ║
+# ╚═══════╝
+# When doing significant work on the docs you probably want to
+# - Set `validation: unrecognized_links: warn` (see rationale below)
+# - Disable `mkdocs-jupyter` which significantly slows build reloads.
+
+site_name: DAPPER
+site_author: Patrick N. Raanes
+site_url: https://nansencenter.github.io/DAPPER
+repo_url: https://github.com/nansencenter/DAPPER
+# edit_uri: edit/master/docs/
+
+docs_dir: docs
+watch: [mkdocs.yml, README.md, dapper]
+nav:
+ - Home: index.md
+ - Reference: reference/
+ - Examples: examples/
+ - dev_guide.md
+ - Bibliography: references.md
+
+validation:
+ # Disabled coz it complains about relative links that work just fine
+ # (and enable interlinking READMEs even on GitHub).
+ # Set to "warn" to help find non-working links.
+ unrecognized_links: ignore
+
+theme:
+ name: material
+ icon:
+ repo: fontawesome/brands/github
+ edit: material/pencil
+ view: material/eye
+ features:
+ - content.code.copy # copy button
+ # - content.code.select # [insiders only]
+ - content.code.annotate # clickable toggler for code comments
+ - navigation.indexes # enable subdir/index.md
+ - toc.follow # ?
+ # - toc.integrate # render ToC as part of left sidebar [not compat with navigation.indexes]
+ - navigation.tabs # top-level sections rendered in header tabs (for wide screens) instead of left sidebar
+ # - navigation.tabs.sticky # don't hide tabs when scrolled
+ # - navigation.expand # expand sections
+ # - navigation.sections # group (rather than collapse) top-level sections
+ - navigation.path # breadcrumbs
+ # - navigation.prune # for large projects
+ - navigation.tracking # update URL to current section
+ - navigation.footer # "Next/Prev"
+ - navigation.instant # SSA
+ - navigation.instant.progress # Progbar on top
+ - navigation.top # "back to top when scrolling up"
+ - header.autohide
+ - search.highlight # highlight matches on page
+ - search.share # deep link to search
+ - search.suggest # search suggestions
+
+ custom_dir: docs/overrides
+
+plugins:
+ - glightbox # zoom functionality
+ # - roamlinks
+ # - blog
+ - search
+ - autorefs # enable anchor/heading links without specifying its page
+ # - meta # [insiders only] enable front-matter defaults as in Jekyll
+ # TODO: enable
+ # - mkdocs-jupyter:
+ # include: ["*.ipynb"] # Default: ["*.py", "*.ipynb"]
+ # ignore:
+ # - ".ipynb_checkpoints/*"
+ # execute: false
+ # - bibtex:
+ # # NB: does not work with mkdocstrings (https://github.com/shyamd/mkdocs-bibtex/issues/246)
+ # # PS: requires pandoc installed on system and markdown_extensions: footnotes
+ # bib_file: docs/bib/refs.bib
+ # bib_by_default: true
+ # csl_file: docs/bib/data-science-journal.csl
+
+ # Autodoc from docstrings:
+ - gen-files: # Genrate .md reflecting .py hierarchy:
+ scripts:
+ - docs/gen_ref_pages.py
+ - literate-nav: # Auto-generate nav
+ nav_file: SUMMARY.md
+ - section-index
+ - mkdocstrings: # Parse docstrings
+ handlers:
+ python:
+ paths: [., dapper]
+ import:
+ - https://docs.python.org/3/objects.inv
+ - https://mkdocstrings.github.io/objects.inv
+ options:
+ # Overview at https://mkdocstrings.github.io/python/usage/?h=show_source#globallocal-options
+ # See sidebar for details.
+ docstring_style: numpy
+ # TODO: activate `show_source` when these [insiders!?] are done
+ # - https://github.com/mkdocstrings/mkdocstrings/issues/525
+ # - https://github.com/mkdocstrings/mkdocstrings/issues/249
+ show_source: false
+ show_symbol_type_heading: true
+ show_symbol_type_toc: true
+ show_root_heading: false
+ summary: false # pretty cool actually
+ show_labels: false
+
+# Mostly from
+# https://squidfunk.github.io/mkdocs-material/setup/extensions/#recommended-configuration
+markdown_extensions:
+ # Python Markdown official extensions
+ - abbr
+ - admonition
+ - def_list
+ - footnotes
+ - toc:
+ title: On this page
+ permalink: ⚓︎
+ toc_depth: 3
+ # Adds the ability to align images, add captions to images (rendering them as figures), and mark large images for lazy-loading:
+ - attr_list
+ - md_in_html
+
+ # Extensions part of Pymdown
+ - pymdownx.arithmatex:
+ generic: true
+ - pymdownx.betterem:
+ smart_enable: all
+ - pymdownx.caret
+ - pymdownx.details
+ - pymdownx.emoji:
+ emoji_index: !!python/name:material.extensions.emoji.twemoji
+ emoji_generator: !!python/name:material.extensions.emoji.to_svg
+ - pymdownx.highlight:
+ anchor_linenums: true
+ line_spans: __span
+ pygments_lang_class: true
+ use_pygments: true
+ - pymdownx.inlinehilite
+ - pymdownx.snippets:
+ base_path:
+ - !relative # dir of current Markdown file
+ # NB: does not work with the virtual files generated by mkdocs-gen-files,
+ # ref https://github.com/oprypin/mkdocs-gen-files/issues/25
+ - !relative $docs_dir # dir of docs
+ - !relative $config_dir # dir of mkdocs.yml
+ - pymdownx.superfences
+ - pymdownx.keys
+ - pymdownx.mark
+ - pymdownx.smartsymbols
+ - pymdownx.tabbed:
+ alternate_style: true
+ - pymdownx.tasklist:
+ custom_checkbox: true
+ - pymdownx.tilde
+
+# hooks:
+# - list_logos.py
+
+extra_javascript:
+ - javascripts/extra.js
+ # For MathJax
+ - javascripts/mathjax.js
+ - https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js
+
+extra_css:
+ - stylesheets/code_select.css
+ - stylesheets/jupyter.css
+
+
+extra:
+ generator: false # "Made with Material for MkDocs"
+ footer:
+ logos:
+ row1:
+ - name: norce.svg
+ height: 50px
+ brightness: 50%
+ - name: nfr.svg
+ height: 80px
+ brightness: 50%
+ row2:
+ - name: akerbp.svg
+ height: 40px
+ brightness: 50%
+ - name: equinor.svg
+ height: 60px
+ brightness: 50%
+ - name: wintershall-dea.svg
+ height: 80px
+ brightness: 70%
diff --git a/pyproject.toml b/pyproject.toml
index d09981a1..651af286 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -80,7 +80,7 @@ exclude = [
"dist",
"site-packages",
"venv",
- ".*", "README.*", "examples/*.ipynb", "autoscaler.py",
+ ".*", "README.*", "docs/examples/*", "autoscaler.py",
]
# Same as Black.
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 00000000..961629b2
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,610 @@
+# This file was autogenerated by uv via the following command:
+# uv pip compile --extra dev setup.py -o requirements.txt
+anyio==4.6.2.post1
+ # via
+ # httpx
+ # jupyter-server
+appnope==0.1.4
+ # via ipykernel
+argon2-cffi==23.1.0
+ # via jupyter-server
+argon2-cffi-bindings==21.2.0
+ # via argon2-cffi
+arrow==1.3.0
+ # via isoduration
+asttokens==2.4.1
+ # via stack-data
+async-lru==2.0.4
+ # via jupyterlab
+attrs==24.2.0
+ # via
+ # jsonschema
+ # referencing
+babel==2.16.0
+ # via
+ # jupyterlab-server
+ # mkdocs-material
+beautifulsoup4==4.12.3
+ # via nbconvert
+bleach==6.2.0
+ # via nbconvert
+cachetools==5.5.0
+ # via tox
+certifi==2024.8.30
+ # via
+ # httpcore
+ # httpx
+ # requests
+cffi==1.17.1
+ # via argon2-cffi-bindings
+cfgv==3.4.0
+ # via pre-commit
+chardet==5.2.0
+ # via tox
+charset-normalizer==3.4.0
+ # via requests
+click==8.1.7
+ # via
+ # mkdocs
+ # mkdocstrings
+colorama==0.4.6
+ # via
+ # dapper (setup.py)
+ # griffe
+ # mkdocs-material
+ # tox
+comm==0.2.2
+ # via
+ # ipykernel
+ # ipywidgets
+contourpy==1.3.0
+ # via matplotlib
+coverage==7.6.4
+ # via
+ # dapper (setup.py)
+ # pytest-cov
+cycler==0.12.1
+ # via matplotlib
+debugpy==1.8.7
+ # via ipykernel
+decorator==5.1.1
+ # via
+ # ipdb
+ # ipython
+defusedxml==0.7.1
+ # via nbconvert
+dill==0.3.8
+ # via
+ # dapper (setup.py)
+ # multiprocess
+ # pathos
+distlib==0.3.9
+ # via virtualenv
+docutils==0.21.2
+ # via readme-renderer
+execnet==2.1.1
+ # via pytest-xdist
+executing==2.1.0
+ # via stack-data
+fastjsonschema==2.20.0
+ # via nbformat
+filelock==3.16.1
+ # via
+ # tox
+ # virtualenv
+fonttools==4.54.1
+ # via matplotlib
+fqdn==1.5.1
+ # via jsonschema
+ghp-import==2.1.0
+ # via mkdocs
+griffe==1.5.1
+ # via mkdocstrings-python
+h11==0.14.0
+ # via httpcore
+httpcore==1.0.6
+ # via httpx
+httpx==0.27.2
+ # via jupyterlab
+identify==2.6.1
+ # via pre-commit
+idna==3.10
+ # via
+ # anyio
+ # httpx
+ # jsonschema
+ # requests
+importlib-metadata==8.5.0
+ # via twine
+iniconfig==2.0.0
+ # via pytest
+ipdb==0.13.13
+ # via dapper (setup.py)
+ipykernel==6.29.5
+ # via
+ # jupyter
+ # jupyter-console
+ # jupyterlab
+ # mkdocs-jupyter
+ipython==8.29.0
+ # via
+ # dapper (setup.py)
+ # ipdb
+ # ipykernel
+ # ipywidgets
+ # jupyter-console
+ipywidgets==8.1.5
+ # via jupyter
+isoduration==20.11.0
+ # via jsonschema
+jaraco-classes==3.4.0
+ # via keyring
+jaraco-context==6.0.1
+ # via keyring
+jaraco-functools==4.1.0
+ # via keyring
+jedi==0.19.1
+ # via ipython
+jinja2==3.1.4
+ # via
+ # jupyter-server
+ # jupyterlab
+ # jupyterlab-server
+ # mkdocs
+ # mkdocs-material
+ # mkdocstrings
+ # nbconvert
+json5==0.9.25
+ # via jupyterlab-server
+jsonpointer==3.0.0
+ # via jsonschema
+jsonschema==4.23.0
+ # via
+ # jupyter-events
+ # jupyterlab-server
+ # nbformat
+jsonschema-specifications==2024.10.1
+ # via jsonschema
+jupyter==1.1.1
+ # via dapper (setup.py)
+jupyter-client==8.6.3
+ # via
+ # ipykernel
+ # jupyter-console
+ # jupyter-server
+ # nbclient
+jupyter-console==6.6.3
+ # via jupyter
+jupyter-core==5.7.2
+ # via
+ # ipykernel
+ # jupyter-client
+ # jupyter-console
+ # jupyter-server
+ # jupyterlab
+ # nbclient
+ # nbconvert
+ # nbformat
+jupyter-events==0.10.0
+ # via jupyter-server
+jupyter-lsp==2.2.5
+ # via jupyterlab
+jupyter-server==2.14.2
+ # via
+ # jupyter-lsp
+ # jupyterlab
+ # jupyterlab-server
+ # notebook
+ # notebook-shim
+jupyter-server-terminals==0.5.3
+ # via jupyter-server
+jupyterlab==4.2.5
+ # via
+ # jupyter
+ # notebook
+jupyterlab-pygments==0.3.0
+ # via nbconvert
+jupyterlab-server==2.27.3
+ # via
+ # jupyterlab
+ # notebook
+jupyterlab-widgets==3.0.13
+ # via ipywidgets
+jupytext==1.16.4
+ # via
+ # dapper (setup.py)
+ # mkdocs-jupyter
+keyring==25.5.0
+ # via twine
+kiwisolver==1.4.7
+ # via matplotlib
+latexcodec==3.0.0
+ # via pybtex
+line-profiler==4.1.3
+ # via dapper (setup.py)
+markdown==3.7
+ # via
+ # mkdocs
+ # mkdocs-autorefs
+ # mkdocs-material
+ # mkdocstrings
+ # pymdown-extensions
+markdown-it-py==3.0.0
+ # via
+ # jupytext
+ # mdit-py-plugins
+ # rich
+markupsafe==3.0.2
+ # via
+ # jinja2
+ # mkdocs
+ # mkdocs-autorefs
+ # mkdocstrings
+ # nbconvert
+matplotlib==3.8.4
+ # via
+ # dapper (setup.py)
+ # mpl-tools
+ # patlib
+matplotlib-inline==0.1.7
+ # via
+ # ipykernel
+ # ipython
+mdit-py-plugins==0.4.2
+ # via jupytext
+mdurl==0.1.2
+ # via markdown-it-py
+mergedeep==1.3.4
+ # via
+ # mkdocs
+ # mkdocs-get-deps
+mistune==3.0.2
+ # via nbconvert
+mkdocs==1.6.1
+ # via
+ # mkdocs-autorefs
+ # mkdocs-gen-files
+ # mkdocs-jupyter
+ # mkdocs-literate-nav
+ # mkdocs-material
+ # mkdocs-section-index
+ # mkdocstrings
+mkdocs-autorefs==1.2.0
+ # via
+ # mkdocstrings
+ # mkdocstrings-python
+mkdocs-gen-files==0.5.0
+ # via dapper (setup.py)
+mkdocs-get-deps==0.2.0
+ # via mkdocs
+mkdocs-glightbox==0.4.0
+ # via dapper (setup.py)
+mkdocs-jupyter==0.25.1
+ # via dapper (setup.py)
+mkdocs-literate-nav==0.6.1
+ # via dapper (setup.py)
+mkdocs-material==9.5.42
+ # via
+ # dapper (setup.py)
+ # mkdocs-jupyter
+mkdocs-material-extensions==1.3.1
+ # via mkdocs-material
+mkdocs-section-index==0.3.9
+ # via dapper (setup.py)
+mkdocstrings==0.26.2
+ # via
+ # dapper (setup.py)
+ # mkdocstrings-python
+mkdocstrings-python==1.12.2
+ # via dapper (setup.py)
+more-itertools==10.5.0
+ # via
+ # jaraco-classes
+ # jaraco-functools
+mpl-tools==0.2.50
+ # via dapper (setup.py)
+multiprocess==0.70.16
+ # via pathos
+nbclient==0.10.0
+ # via nbconvert
+nbconvert==7.16.4
+ # via
+ # jupyter
+ # jupyter-server
+ # mkdocs-jupyter
+nbformat==5.10.4
+ # via
+ # jupyter-server
+ # jupytext
+ # nbclient
+ # nbconvert
+nest-asyncio==1.6.0
+ # via ipykernel
+nh3==0.2.18
+ # via readme-renderer
+nodeenv==1.9.1
+ # via pre-commit
+notebook==7.2.2
+ # via jupyter
+notebook-shim==0.2.4
+ # via
+ # jupyterlab
+ # notebook
+numpy==1.26.4
+ # via
+ # dapper (setup.py)
+ # contourpy
+ # matplotlib
+ # patlib
+ # scipy
+overrides==7.7.0
+ # via jupyter-server
+packaging==24.1
+ # via
+ # ipykernel
+ # jupyter-server
+ # jupyterlab
+ # jupyterlab-server
+ # jupytext
+ # matplotlib
+ # mkdocs
+ # nbconvert
+ # pyproject-api
+ # pytest
+ # pytest-sugar
+ # tox
+paginate==0.5.7
+ # via mkdocs-material
+pandocfilters==1.5.1
+ # via nbconvert
+parso==0.8.4
+ # via jedi
+pathos==0.3.2
+ # via dapper (setup.py)
+pathspec==0.12.1
+ # via mkdocs
+patlib==0.3.5
+ # via dapper (setup.py)
+pexpect==4.9.0
+ # via ipython
+pillow==11.0.0
+ # via matplotlib
+pkginfo==1.10.0
+ # via twine
+platformdirs==4.3.6
+ # via
+ # jupyter-core
+ # mkdocs-get-deps
+ # mkdocstrings
+ # tox
+ # virtualenv
+pluggy==1.5.0
+ # via
+ # pytest
+ # tox
+pox==0.3.5
+ # via pathos
+ppft==1.7.6.9
+ # via pathos
+pprintpp==0.4.0
+ # via pytest-clarity
+pre-commit==4.0.1
+ # via dapper (setup.py)
+prometheus-client==0.21.0
+ # via jupyter-server
+prompt-toolkit==3.0.48
+ # via
+ # ipython
+ # jupyter-console
+psutil==6.1.0
+ # via ipykernel
+ptyprocess==0.7.0
+ # via
+ # pexpect
+ # terminado
+pure-eval==0.2.3
+ # via stack-data
+py-cpuinfo==9.0.0
+ # via pytest-benchmark
+pybtex==0.24.0
+ # via dapper (setup.py)
+pycparser==2.22
+ # via cffi
+pygments==2.18.0
+ # via
+ # ipython
+ # jupyter-console
+ # mkdocs-jupyter
+ # mkdocs-material
+ # nbconvert
+ # readme-renderer
+ # rich
+pymdown-extensions==10.12
+ # via
+ # mkdocs-material
+ # mkdocstrings
+pyparsing==3.2.0
+ # via matplotlib
+pyproject-api==1.8.0
+ # via tox
+pytest==8.3.3
+ # via
+ # dapper (setup.py)
+ # pytest-benchmark
+ # pytest-clarity
+ # pytest-cov
+ # pytest-sugar
+ # pytest-timeout
+ # pytest-xdist
+pytest-benchmark==5.1.0
+ # via dapper (setup.py)
+pytest-clarity==1.0.1
+ # via dapper (setup.py)
+pytest-cov==6.0.0
+ # via dapper (setup.py)
+pytest-sugar==1.0.0
+ # via dapper (setup.py)
+pytest-timeout==2.3.1
+ # via dapper (setup.py)
+pytest-xdist==3.6.1
+ # via dapper (setup.py)
+python-dateutil==2.9.0.post0
+ # via
+ # arrow
+ # ghp-import
+ # jupyter-client
+ # matplotlib
+python-json-logger==2.0.7
+ # via jupyter-events
+pyyaml==6.0.2
+ # via
+ # dapper (setup.py)
+ # jupyter-events
+ # jupytext
+ # mkdocs
+ # mkdocs-get-deps
+ # pre-commit
+ # pybtex
+ # pymdown-extensions
+ # pyyaml-env-tag
+pyyaml-env-tag==0.1
+ # via mkdocs
+pyzmq==26.2.0
+ # via
+ # ipykernel
+ # jupyter-client
+ # jupyter-console
+ # jupyter-server
+readme-renderer==44.0
+ # via twine
+referencing==0.35.1
+ # via
+ # jsonschema
+ # jsonschema-specifications
+ # jupyter-events
+regex==2024.9.11
+ # via mkdocs-material
+requests==2.32.3
+ # via
+ # jupyterlab-server
+ # mkdocs-material
+ # requests-toolbelt
+ # twine
+requests-toolbelt==1.0.0
+ # via twine
+rfc3339-validator==0.1.4
+ # via
+ # jsonschema
+ # jupyter-events
+rfc3986==2.0.0
+ # via twine
+rfc3986-validator==0.1.1
+ # via
+ # jsonschema
+ # jupyter-events
+rich==13.9.3
+ # via
+ # pytest-clarity
+ # twine
+rpds-py==0.20.0
+ # via
+ # jsonschema
+ # referencing
+ruff==0.7.1
+ # via dapper (setup.py)
+scipy==1.14.1
+ # via
+ # dapper (setup.py)
+ # mpl-tools
+ # patlib
+send2trash==1.8.3
+ # via jupyter-server
+setuptools==75.3.0
+ # via jupyterlab
+six==1.16.0
+ # via
+ # asttokens
+ # pybtex
+ # python-dateutil
+ # rfc3339-validator
+sniffio==1.3.1
+ # via
+ # anyio
+ # httpx
+soupsieve==2.6
+ # via beautifulsoup4
+stack-data==0.6.3
+ # via ipython
+struct-tools==0.2.5
+ # via dapper (setup.py)
+tabulate==0.8.10
+ # via dapper (setup.py)
+termcolor==2.5.0
+ # via pytest-sugar
+terminado==0.18.1
+ # via
+ # jupyter-server
+ # jupyter-server-terminals
+threadpoolctl==3.5.0
+ # via dapper (setup.py)
+tinycss2==1.4.0
+ # via nbconvert
+tornado==6.4.1
+ # via
+ # dapper (setup.py)
+ # ipykernel
+ # jupyter-client
+ # jupyter-server
+ # jupyterlab
+ # notebook
+ # terminado
+tox==4.23.2
+ # via dapper (setup.py)
+tqdm==4.66.6
+ # via dapper (setup.py)
+traitlets==5.14.3
+ # via
+ # comm
+ # ipykernel
+ # ipython
+ # ipywidgets
+ # jupyter-client
+ # jupyter-console
+ # jupyter-core
+ # jupyter-events
+ # jupyter-server
+ # jupyterlab
+ # matplotlib-inline
+ # nbclient
+ # nbconvert
+ # nbformat
+twine==5.1.1
+ # via dapper (setup.py)
+types-python-dateutil==2.9.0.20241003
+ # via arrow
+uri-template==1.3.0
+ # via jsonschema
+urllib3==2.2.3
+ # via
+ # requests
+ # twine
+virtualenv==20.27.1
+ # via
+ # pre-commit
+ # tox
+watchdog==5.0.3
+ # via mkdocs
+wcwidth==0.2.13
+ # via prompt-toolkit
+webcolors==24.8.0
+ # via jsonschema
+webencodings==0.5.1
+ # via
+ # bleach
+ # tinycss2
+websocket-client==1.8.0
+ # via jupyter-server
+widgetsnbextension==4.0.13
+ # via ipywidgets
+zipp==3.20.2
+ # via importlib-metadata
diff --git a/setup.py b/setup.py
index b3981d3a..038b3363 100755
--- a/setup.py
+++ b/setup.py
@@ -22,6 +22,7 @@
"scipy>=1.10",
"numpy~=1.20",
"jupyter",
+ "notebook<7",
"ipdb",
"ipython>=5.1",
"tornado~=6.3", # 6.2 breaks Jupyter plots (tested on local Mac, Linux)
@@ -53,10 +54,23 @@
"pytest-timeout",
],
"lint": ["ruff"],
+ "doc": [
+ "mkdocs-material",
+ "mkdocstrings",
+ "mkdocstrings-python",
+ "mkdocs-gen-files",
+ "mkdocs-literate-nav",
+ "mkdocs-section-index",
+ "mkdocs-glightbox",
+ "mkdocs-jupyter",
+ "pybtex",
+ ],
# 'flake8-docstrings', 'flake8-bugbear', 'flake8-comprehensions'],
- "build": ["twine", "pdoc", "jupytext"],
+ "build": ["twine", "jupytext"],
}
-EXTRAS["dev"] = EXTRAS["debug"] + EXTRAS["test"] + EXTRAS["lint"] + EXTRAS["build"]
+EXTRAS["dev"] = (
+ EXTRAS["debug"] + EXTRAS["test"] + EXTRAS["lint"] + EXTRAS["build"] + EXTRAS["doc"]
+)
def find_version(*file_paths):
@@ -100,7 +114,7 @@ def read(*parts):
install_requires=INSTALL_REQUIRES,
extras_require=EXTRAS,
packages=find_packages(),
- py_modules=["examples.basic_1", "examples.basic_2", "examples.basic_3"],
+ # py_modules=["examples.basic_1", "examples.basic_2", "examples.basic_3"],
package_data={
"": ["*.txt", "*.md", "*.png", "*.yaml"],
"dapper.mods.QG.f90": ["*.txt", "*.md", "*.png", "Makefile", "*.f90"],
diff --git a/tests/test_data.py b/tests/test_data.py
index 95ac52da..de3b9551 100644
--- a/tests/test_data.py
+++ b/tests/test_data.py
@@ -1,4 +1,4 @@
-"""Test data loading and `dapper.xp_process.xpSpace.print`.
+"""Test data loading and [`xp_process.xpSpace.print`][].
If script is run with one of these command-line arguments,
then there is no test generation, but rather:
@@ -65,7 +65,7 @@ def cap_stdout(fun, *args, **kwargs):
def gen_test_set(xp_dict, *args, **kwargs):
- """Capture printout of `dapper.xp_process.xpSpace.print`, gen tests."""
+ """Capture printout of [`xp_process.xpSpace.print`][], gen tests."""
# Get stdout of xpSpace.print()
output = cap_stdout(xp_dict.print, *args, **kwargs, colorize=False)
diff --git a/tests/test_round2.py b/tests/test_round2.py
index 6870db0f..5ec6ca2f 100644
--- a/tests/test_round2.py
+++ b/tests/test_round2.py
@@ -9,7 +9,8 @@
class ca(float):
"""Make `==` approximate.
- Example:
+ Examples
+ --------
>>> ca(1 + 1e-6) == 1
True