diff --git a/python/vegafusion-jupyter/conda-win-64-cp310.lock b/python/vegafusion-jupyter/conda-win-64-cp310.lock
index a33b2ec7b..18eb4f841 100644
--- a/python/vegafusion-jupyter/conda-win-64-cp310.lock
+++ b/python/vegafusion-jupyter/conda-win-64-cp310.lock
@@ -1,12 +1,12 @@
# Generated by conda-lock.
# platform: win-64
-# input_hash: 1dc33e11a55c85392a21bf6d56c3fc4803e0c66a5c9aea72c0f582bc1bb7c9c3
+# input_hash: 32fabb56021a7b0a6e2f2322cc88d9fee399e2238a8dbc3f6078a1aa4b627947
@EXPLICIT
https://conda.anaconda.org/conda-forge/win-64/ca-certificates-2021.10.8-h5b45459_0.tar.bz2#2ddd48c9b52f7f65361b9645b2c5d370
https://conda.anaconda.org/conda-forge/win-64/intel-openmp-2022.0.0-h57928b3_3663.tar.bz2#9617f0042f5eea1155970e6861f3ab6b
https://conda.anaconda.org/conda-forge/win-64/msys2-conda-epoch-20160418-1.tar.bz2#b0309b72560df66f71a9d5e34a5efdfa
-https://conda.anaconda.org/conda-forge/win-64/nodejs-17.1.0-h57928b3_2.tar.bz2#0fd78a8cb239257a30ab38b94840bc0d
-https://conda.anaconda.org/conda-forge/win-64/pandoc-2.17-h8ffe710_0.tar.bz2#f2cea2040268a53502f647b04d87c182
+https://conda.anaconda.org/conda-forge/win-64/nodejs-17.4.0-h57928b3_0.tar.bz2#88f3519bd8fbf93ea05e72ceaaf391db
+https://conda.anaconda.org/conda-forge/win-64/pandoc-2.17.1.1-h57928b3_0.tar.bz2#75ecf3cc83aad34874efde6f93d48c67
https://conda.anaconda.org/conda-forge/noarch/tzdata-2021e-he74cb21_0.tar.bz2#a751ec502589ebdc2eceb183ff602569
https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.20348.0-h57928b3_0.tar.bz2#6d666b6ea8251231ff508062d1e41f9c
https://conda.anaconda.org/conda-forge/win-64/winpty-0.4.3-4.tar.bz2#1cee351bf20b830d991dbe0bc8cd7dfe
@@ -18,18 +18,19 @@ https://conda.anaconda.org/conda-forge/win-64/m2w64-gcc-libs-core-5.3.0-7.tar.bz
https://conda.anaconda.org/conda-forge/win-64/vc-14.2-hb210afc_6.tar.bz2#c2aecbc9b00ba6f352e27d3d61fd31fb
https://conda.anaconda.org/conda-forge/win-64/blosc-1.21.0-h0e60522_0.tar.bz2#19b82b554c46598cc5cc01a58f30f547
https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h8ffe710_4.tar.bz2#7c03c66026944073040cb19a4f3ec3c9
-https://conda.anaconda.org/conda-forge/win-64/charls-2.2.0-h39d44d4_0.tar.bz2#7b4af3de4c91113430f2cafec2221764
+https://conda.anaconda.org/conda-forge/win-64/charls-2.3.4-h39d44d4_0.tar.bz2#bfbea80de69a6ceed0a717f63a91d326
+https://conda.anaconda.org/conda-forge/win-64/fribidi-1.0.10-h8d14728_0.tar.bz2#807e81d915f2bb2e49951648615241f6
https://conda.anaconda.org/conda-forge/win-64/giflib-5.2.1-h8d14728_2.tar.bz2#c577cd3be126d95e9a05ee5aa9d2875b
https://conda.anaconda.org/conda-forge/win-64/jbig-2.1-h8d14728_2003.tar.bz2#37dcc26d63c315f6c0588579dca810da
-https://conda.anaconda.org/conda-forge/win-64/jpeg-9d-h8ffe710_0.tar.bz2#9335a1b24eefef6075f4c02a03baf27a
+https://conda.anaconda.org/conda-forge/win-64/jpeg-9e-h8ffe710_0.tar.bz2#83e0284f1306a3381a825d7a728aacc2
https://conda.anaconda.org/conda-forge/win-64/jxrlib-1.1-h8ffe710_2.tar.bz2#69f82948e102dc14928619140c29468d
https://conda.anaconda.org/conda-forge/win-64/lerc-3.0-h0e60522_0.tar.bz2#756c8b51a32758df2ed6cddcc7b7ed58
https://conda.anaconda.org/conda-forge/win-64/libaec-1.0.6-h39d44d4_0.tar.bz2#ac78b243f1ee03a2412b6e328aa3a12d
https://conda.anaconda.org/conda-forge/win-64/libbrotlicommon-1.0.9-h8ffe710_6.tar.bz2#e1611959ec9329ad406b2f5c84b8757f
-https://conda.anaconda.org/conda-forge/win-64/libdeflate-1.8-h8ffe710_0.tar.bz2#8d90ca64db6b511cd5d3ab8b675f4c20
+https://conda.anaconda.org/conda-forge/win-64/libdeflate-1.10-h8ffe710_0.tar.bz2#ad4246997621fdf913fe6f958bc16fd4
https://conda.anaconda.org/conda-forge/win-64/libffi-3.4.2-h8ffe710_5.tar.bz2#2c96d1b6915b408893f9472569dee135
https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.18-h8d14728_1.tar.bz2#5c1fb45b5e2912c19098750ae8a32604
-https://conda.anaconda.org/conda-forge/win-64/libwebp-base-1.2.1-h8ffe710_0.tar.bz2#3bd686e19cc9722e674e2f8119bc476d
+https://conda.anaconda.org/conda-forge/win-64/libwebp-base-1.2.2-h8ffe710_1.tar.bz2#24e23990217d3542fb821759a41d6ec2
https://conda.anaconda.org/conda-forge/win-64/libzlib-1.2.11-h8ffe710_1013.tar.bz2#b28dd2488b4e5f892c67071acc1d0a8c
https://conda.anaconda.org/conda-forge/win-64/libzopfli-1.0.3-h0e60522_0.tar.bz2#b4b0cbc0abc9f26b730231ffdabf3881
https://conda.anaconda.org/conda-forge/win-64/lz4-c-1.9.3-h8ffe710_1.tar.bz2#d12763533276560a931c1bd3df1adf63
@@ -38,19 +39,20 @@ https://conda.anaconda.org/conda-forge/win-64/openssl-1.1.1l-h8ffe710_0.tar.bz2#
https://conda.anaconda.org/conda-forge/win-64/snappy-1.1.8-ha925a31_3.tar.bz2#e227e973dec9f437b28b24f9f0e5f7fe
https://conda.anaconda.org/conda-forge/win-64/sqlite-3.37.0-h8ffe710_0.tar.bz2#cc2d704449f994c1aa422a5a1cd8a64e
https://conda.anaconda.org/conda-forge/win-64/tbb-2021.5.0-h2d74725_0.tar.bz2#81892af9e08d8bf7a98e9713189d6885
-https://conda.anaconda.org/conda-forge/win-64/tk-8.6.11-h8ffe710_1.tar.bz2#fd3c141a1ed5a77e5813795950fb7395
+https://conda.anaconda.org/conda-forge/win-64/tk-8.6.12-h8ffe710_0.tar.bz2#c69a5047cc9291ae40afd4a1ad6f0c0f
https://conda.anaconda.org/conda-forge/win-64/xz-5.2.5-h62dcd97_1.tar.bz2#eabcbfedd14d7c18a514afca09ea0ebb
https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h8ffe710_2.tar.bz2#adbfb9f45d1004a26763652246a33764
https://conda.anaconda.org/conda-forge/win-64/zfp-0.5.5-h0e60522_8.tar.bz2#01afc300e6617dbaad37fe834af41b11
https://conda.anaconda.org/conda-forge/win-64/krb5-1.19.2-h20d022d_3.tar.bz2#8a773a26af200fd95350e0866c02e2b3
https://conda.anaconda.org/conda-forge/win-64/libbrotlidec-1.0.9-h8ffe710_6.tar.bz2#147294ddd621c7a5288af3393c711760
https://conda.anaconda.org/conda-forge/win-64/libbrotlienc-1.0.9-h8ffe710_6.tar.bz2#a9d521d66b91eea8c265931032ac948e
+https://conda.anaconda.org/conda-forge/win-64/libwebp-1.2.2-h57928b3_0.tar.bz2#80c0ce63cf8f69c5a5781ab7a9645942
https://conda.anaconda.org/conda-forge/win-64/m2w64-gcc-libs-5.3.0-7.tar.bz2#fe759119b8b3bfa720b8762c6fdc35de
-https://conda.anaconda.org/conda-forge/win-64/mkl-2021.4.0-h0e2418a_729.tar.bz2#42fcb45077a716cb8d967117b8b88f28
-https://conda.anaconda.org/conda-forge/win-64/python-3.10.2-h9a09f29_0_cpython.tar.bz2#a71076b607b8d8828d0a64f533022f83
+https://conda.anaconda.org/conda-forge/win-64/mkl-2022.0.0-h0e2418a_796.tar.bz2#54baa34953c02445bfd6f566c45decbb
+https://conda.anaconda.org/conda-forge/win-64/python-3.10.2-h9a09f29_3_cpython.tar.bz2#81ec8e7d9481cd2963c302b7bb565d87
https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.4-h0e60522_1.tar.bz2#e1aff0583dda5fb917eb3d2c1025aa80
https://conda.anaconda.org/conda-forge/win-64/zlib-1.2.11-h8ffe710_1013.tar.bz2#866517df4fd8bb813bc20c24cf7b8f05
-https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.1-h6255e5f_0.tar.bz2#cf642c6d2533de5340d314027d5fba0d
+https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.2-h6255e5f_0.tar.bz2#68de506283d23d66cf1e4ce3814d6cdc
https://conda.anaconda.org/conda-forge/noarch/appdirs-1.4.4-pyh9f0ad1d_0.tar.bz2#5f095bc6454094e96f146491fd03633b
https://conda.anaconda.org/conda-forge/noarch/async_generator-1.10-py_0.tar.bz2#d56c596e61b1c4952acf0a9920856c12
https://conda.anaconda.org/conda-forge/noarch/atomicwrites-1.4.0-pyh9f0ad1d_0.tar.bz2#5e36230ffaf3b7bb599424592684ae53
@@ -59,37 +61,42 @@ https://conda.anaconda.org/conda-forge/noarch/backcall-0.2.0-pyh9f0ad1d_0.tar.bz
https://conda.anaconda.org/conda-forge/noarch/backports-1.0-py_2.tar.bz2#0da16b293affa6ac31812376f8eb79dd
https://conda.anaconda.org/conda-forge/win-64/brotli-bin-1.0.9-h8ffe710_6.tar.bz2#94163a6bf564144f1b163535624ca4fc
https://conda.anaconda.org/conda-forge/win-64/c-blosc2-2.0.4-h09319c2_1.tar.bz2#70ae80e8d7cd5382b1ae6ef63248c9d3
-https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-2.0.10-pyhd8ed1ab_0.tar.bz2#ea77236c8031cfa821720b21b4cb0ceb
+https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-2.0.12-pyhd8ed1ab_0.tar.bz2#1f5b32dabae0f1893ae3283dac7f799e
https://conda.anaconda.org/conda-forge/noarch/cloudpickle-2.0.0-pyhd8ed1ab_0.tar.bz2#3a8fc8b627d5fb6af827e126a10a86c6
https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.4-pyh9f0ad1d_0.tar.bz2#c08b4c1326b880ed44f3ffb04803332f
https://conda.anaconda.org/conda-forge/noarch/cycler-0.11.0-pyhd8ed1ab_0.tar.bz2#a50559fad0affdbb33729a68669ca1cb
+https://conda.anaconda.org/conda-forge/noarch/dataclasses-0.8-pyhc8e2a94_3.tar.bz2#a362b2124b06aad102e2ee4581acee7d
https://conda.anaconda.org/conda-forge/noarch/decorator-5.1.1-pyhd8ed1ab_0.tar.bz2#43afe5ab04e35e17ba28649471dd7364
https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2#961b3a227b437d82ad7054484cfa71b2
-https://conda.anaconda.org/conda-forge/noarch/entrypoints-0.3-pyhd8ed1ab_1003.tar.bz2#bbf9a201f6ce99a506f4955374d9a9f4
-https://conda.anaconda.org/conda-forge/noarch/filelock-3.4.2-pyhd8ed1ab_1.tar.bz2#d3f5797d3f9625c64860c93fc4359e64
+https://conda.anaconda.org/conda-forge/noarch/entrypoints-0.4-pyhd8ed1ab_0.tar.bz2#3cf04868fee0a029769bd41f4b2fbf2d
+https://conda.anaconda.org/conda-forge/noarch/executing-0.8.2-pyhd8ed1ab_0.tar.bz2#dcd884e2cf5bcdccdf78f7db35999d62
+https://conda.anaconda.org/conda-forge/noarch/filelock-3.5.1-pyhd8ed1ab_0.tar.bz2#e2587087f4809c3fb6dc4c29648ecfc8
https://conda.anaconda.org/conda-forge/noarch/flaky-3.7.0-pyh9f0ad1d_0.tar.bz2#d20be9ed08052d16593c88d51b774a82
https://conda.anaconda.org/conda-forge/noarch/flit-core-3.6.0-pyhd8ed1ab_0.tar.bz2#f8c17f22a3ce533876c468157ff8ff8f
https://conda.anaconda.org/conda-forge/noarch/fsspec-2022.1.0-pyhd8ed1ab_0.tar.bz2#188e095f4dc38887bb48b065734b9e8d
https://conda.anaconda.org/conda-forge/noarch/h11-0.12.0-pyhd8ed1ab_0.tar.bz2#a8c3c313e5339029946b66070cf24b39
-https://conda.anaconda.org/conda-forge/noarch/idna-3.1-pyhd3deb0d_0.tar.bz2#9c9aea4b8391264477df484f798562d0
+https://conda.anaconda.org/conda-forge/noarch/idna-3.3-pyhd8ed1ab_0.tar.bz2#40b50b8b030f5f2f22085c062ed013dd
https://conda.anaconda.org/conda-forge/noarch/iniconfig-1.1.1-pyh9f0ad1d_0.tar.bz2#39161f81cc5e5ca45b8226fbb06c6905
https://conda.anaconda.org/conda-forge/noarch/ipython_genutils-0.2.0-py_1.tar.bz2#5071c982548b3a20caf70462f04f5287
https://conda.anaconda.org/conda-forge/noarch/json5-0.9.5-pyh9f0ad1d_0.tar.bz2#10759827a94e6b14996e81fb002c0bda
-https://conda.anaconda.org/conda-forge/win-64/libblas-3.9.0-12_win64_mkl.tar.bz2#96b3e24fd626a2a964c81045da31c7b1
+https://conda.anaconda.org/conda-forge/win-64/libblas-3.9.0-13_win64_mkl.tar.bz2#5addc4aa3bae7e89ce6eb8720650b257
https://conda.anaconda.org/conda-forge/win-64/libpng-1.6.37-h1d00b33_2.tar.bz2#005ddb14b8f876ed6a85b76dfc9892db
https://conda.anaconda.org/conda-forge/win-64/libssh2-1.10.0-h680486a_2.tar.bz2#7c3dab9db46226852f9c4c163870e4e4
-https://conda.anaconda.org/conda-forge/win-64/libtiff-4.3.0-hd413186_2.tar.bz2#40718c1376954db41607d76ebb81e1b3
+https://conda.anaconda.org/conda-forge/win-64/libtiff-4.3.0-hc4061b1_3.tar.bz2#2e9f4af7dd16047f2f370562fb9e81fc
https://conda.anaconda.org/conda-forge/noarch/locket-0.2.0-py_2.tar.bz2#709e8671651c7ec3d1ad07800339ff1d
https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyh9f0ad1d_0.tar.bz2#2ba8498c1018c1e9c61eb99b973dfe19
https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.5.4-pyhd8ed1ab_0.tar.bz2#0d86e4e6ac78912f3f47e0453b124aca
-https://conda.anaconda.org/conda-forge/noarch/olefile-0.46-pyh9f0ad1d_1.tar.bz2#0b2e68acc8c78c8cc392b90983481f58
https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2#457c2c8c08e54905d6954e79cb5b5db9
https://conda.anaconda.org/conda-forge/noarch/parso-0.8.3-pyhd8ed1ab_0.tar.bz2#17a565a0c3899244e938cdf417e7b094
+https://conda.anaconda.org/conda-forge/noarch/pathspec-0.9.0-pyhd8ed1ab_0.tar.bz2#f93dc0ccbc0a8472624165f6e256c7d1
https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-py_1003.tar.bz2#415f0ebb6198cc2801c73438a9fb5761
-https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.12.0-pyhd8ed1ab_0.tar.bz2#d7b366cccdf2c1e8ebdddbfff7c2ba00
+https://conda.anaconda.org/conda-forge/noarch/platformdirs-2.5.0-pyhd8ed1ab_0.tar.bz2#7fe854bc6252760836b0f0ab6ced04ce
+https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.13.1-pyhd8ed1ab_0.tar.bz2#9259acd5dd8746e7721474fa62e046d7
+https://conda.anaconda.org/conda-forge/win-64/pthread-stubs-0.4-hcd874cb_1001.tar.bz2#a1f820480193ea83582b13249a7e7bd9
+https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.2-pyhd8ed1ab_0.tar.bz2#6784285c7e55cb7212efabc79e4c2883
https://conda.anaconda.org/conda-forge/noarch/py-1.11.0-pyh6c4a22f_0.tar.bz2#b4613d7e7a493916d867842a6a148054
https://conda.anaconda.org/conda-forge/noarch/pycparser-2.21-pyhd8ed1ab_0.tar.bz2#076becd9e05608f8dc72757d5f3a91ff
-https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.0.6-pyhd8ed1ab_0.tar.bz2#3087df8c636c5a00e694605c39ce4982
+https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.0.7-pyhd8ed1ab_0.tar.bz2#727e2216d9c47455d8ddc060eb2caad9
https://conda.anaconda.org/conda-forge/win-64/python_abi-3.10-2_cp310.tar.bz2#aaa900b98edb2e67106b461ff365ba57
https://conda.anaconda.org/conda-forge/noarch/pytz-2021.3-pyhd8ed1ab_0.tar.bz2#7e4f811bff46a5a6a7e0094921389395
https://conda.anaconda.org/conda-forge/noarch/send2trash-1.8.0-pyhd8ed1ab_0.tar.bz2#edab14119efe85c3bf131ad747e9005c
@@ -98,115 +105,124 @@ https://conda.anaconda.org/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_
https://conda.anaconda.org/conda-forge/noarch/tenacity-8.0.1-pyhd8ed1ab_0.tar.bz2#8b29b2c12cb21dbd057755e5fd22d005
https://conda.anaconda.org/conda-forge/noarch/testpath-0.5.0-pyhd8ed1ab_0.tar.bz2#53b57d6a468bebc7cef1253b177a5e9e
https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2#f832c45a477c78bebd107098db465095
-https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.0-pyhd8ed1ab_1.tar.bz2#9b49de2db8b4a4a48c424629694fbd37
+https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2#5844808ffab9ebdb694585b50ba02a96
https://conda.anaconda.org/conda-forge/noarch/toolz-0.11.2-pyhd8ed1ab_0.tar.bz2#f348d1590550371edfac5ed3c1d44f7e
https://conda.anaconda.org/conda-forge/noarch/traitlets-5.1.1-pyhd8ed1ab_0.tar.bz2#a1bc9765ef9499760e88f568b3a6622c
https://conda.anaconda.org/conda-forge/noarch/typing-3.10.0.0-pyhd8ed1ab_0.tar.bz2#e6573ac68718f17b9d4f5c8eda3190f2
-https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.0.1-pyha770c72_0.tar.bz2#1fc03816925d3cb7fdab9ab234e7fea7
+https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.1.1-pyha770c72_0.tar.bz2#74761ba7bc682e9009520163a1031ace
https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-py_1.tar.bz2#3563be4c5611a44210d9ba0c16113136
https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.2.3-pyhd8ed1ab_0.tar.bz2#a9e89668df0da0f33c8c4ddf7c118f6d
https://conda.anaconda.org/conda-forge/noarch/wheel-0.37.1-pyhd8ed1ab_0.tar.bz2#1ca02aaf78d9c70d9a81a3bed5752022
-https://conda.anaconda.org/conda-forge/noarch/zipp-3.7.0-pyhd8ed1ab_0.tar.bz2#947f7f41958eabc0f6e886557512bb76
+https://conda.anaconda.org/conda-forge/win-64/xorg-libxau-1.0.9-hcd874cb_0.tar.bz2#9cef622e75683c17d05ae62d66e69e6c
+https://conda.anaconda.org/conda-forge/win-64/xorg-libxdmcp-1.1.3-hcd874cb_0.tar.bz2#46878ebb6b9cbd8afcf8088d7ef00ece
+https://conda.anaconda.org/conda-forge/noarch/zipp-3.7.0-pyhd8ed1ab_1.tar.bz2#b689b2cbc8481b224777415e1a193170
+https://conda.anaconda.org/conda-forge/noarch/asttokens-2.0.5-pyhd8ed1ab_0.tar.bz2#74badce16f060701fee55c39332f5253
https://conda.anaconda.org/conda-forge/noarch/babel-2.9.1-pyh44b312d_0.tar.bz2#74136ed39bfea0832d338df1e58d013e
https://conda.anaconda.org/conda-forge/win-64/brotli-1.0.9-h8ffe710_6.tar.bz2#d92e55db1cc5e34d1ba0812366c29826
https://conda.anaconda.org/conda-forge/win-64/certifi-2021.10.8-py310h5588dad_1.tar.bz2#d54dc314d865498a7349f4ebe35448a6
https://conda.anaconda.org/conda-forge/win-64/cffi-1.15.0-py310hcbf9ad4_0.tar.bz2#ffea8be6c3b061eb7361a8686af44861
https://conda.anaconda.org/conda-forge/win-64/click-8.0.3-py310h5588dad_1.tar.bz2#e8411019db120f883547b546dc3503a3
-https://conda.anaconda.org/conda-forge/win-64/coverage-6.2-py310he2412df_0.tar.bz2#8133d9e7a63ace0be562f04181ae0890
+https://conda.anaconda.org/conda-forge/win-64/coverage-6.3.1-py310he2412df_0.tar.bz2#81e085bdc5a38a5be11f23e2909646bd
https://conda.anaconda.org/conda-forge/win-64/cytoolz-0.11.2-py310he2412df_1.tar.bz2#b1f0199fd7a7610c214d19343bb095f6
https://conda.anaconda.org/conda-forge/win-64/debugpy-1.5.1-py310h8a704f9_0.tar.bz2#5882f83cd4d4213bdea77fb4219a32b2
https://conda.anaconda.org/conda-forge/win-64/freetype-2.10.4-h546665d_1.tar.bz2#1215a2e49d23da91c28d97cff8de35ea
-https://conda.anaconda.org/conda-forge/win-64/importlib-metadata-4.10.0-py310h5588dad_0.tar.bz2#01b21d7c88a60c78879144c1c7de78a3
+https://conda.anaconda.org/conda-forge/win-64/importlib-metadata-4.11.1-py310h5588dad_0.tar.bz2#9b44cfcdd177ccc9d9c320ed6b5b839d
https://conda.anaconda.org/conda-forge/noarch/importlib_resources-5.4.0-pyhd8ed1ab_0.tar.bz2#9fb134dbabe7851a9d71411064b2c30d
https://conda.anaconda.org/conda-forge/win-64/jedi-0.18.1-py310h5588dad_0.tar.bz2#f61b25ec030168c94b25cf20153cef3b
https://conda.anaconda.org/conda-forge/win-64/kiwisolver-1.3.2-py310h476a331_1.tar.bz2#298c6a53b7685e97ef663a85a3148894
https://conda.anaconda.org/conda-forge/win-64/lcms2-2.12-h2a16943_0.tar.bz2#fee639c27301c4165b4d1f7e442de8a5
-https://conda.anaconda.org/conda-forge/win-64/libcblas-3.9.0-12_win64_mkl.tar.bz2#5103ff857d4086ae01f2df8757f31d0b
+https://conda.anaconda.org/conda-forge/win-64/libcblas-3.9.0-13_win64_mkl.tar.bz2#0facf207b485dc20c4ef5f8b92c7c949
https://conda.anaconda.org/conda-forge/win-64/libcurl-7.81.0-h789b8ee_0.tar.bz2#c5ac1ff403590d7b4472d5e89adfbd32
-https://conda.anaconda.org/conda-forge/win-64/liblapack-3.9.0-12_win64_mkl.tar.bz2#e2971c6292cb0daf16bced80b9aa767a
+https://conda.anaconda.org/conda-forge/win-64/liblapack-3.9.0-13_win64_mkl.tar.bz2#4a777de294375e4d78ad53e5581a9ede
+https://conda.anaconda.org/conda-forge/win-64/libxcb-1.13-hcd874cb_1004.tar.bz2#a6d7fd030532378ecb6ba435cd9f8234
https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-1.1.0-pyhd8ed1ab_0.tar.bz2#84e8dfb1a9e6a824f32fd45b867271ca
https://conda.anaconda.org/conda-forge/win-64/markupsafe-2.0.1-py310he2412df_1.tar.bz2#b4da490d6d77767dd98e58d0dd3858e0
https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.3-pyhd8ed1ab_0.tar.bz2#be3bfd435802d2c768c6b2439f325f3d
-https://conda.anaconda.org/conda-forge/win-64/maturin-0.12.6-py310hdc45392_0.tar.bz2#7450d594d7c13d1aa3de080f13f72a78
+https://conda.anaconda.org/conda-forge/win-64/maturin-0.12.8-py310hdc45392_0.tar.bz2#8ab039d48eb1d5ed330b6cb74548fcbd
https://conda.anaconda.org/conda-forge/win-64/mistune-0.8.4-py310he2412df_1005.tar.bz2#b845e34a59732681cdd63f5c5d3456b4
+https://conda.anaconda.org/conda-forge/win-64/mypy_extensions-0.4.3-py310h5588dad_4.tar.bz2#52c10573dce166bb8a8318956dc6e1c4
https://conda.anaconda.org/conda-forge/win-64/openjpeg-2.4.0-hb211442_1.tar.bz2#0991d2e943e5ba7ec9b7b32eec14e2e3
https://conda.anaconda.org/conda-forge/noarch/outcome-1.1.0-pyhd8ed1ab_0.tar.bz2#a6baaddfb9fb5aa6b53534b634cb69c9
https://conda.anaconda.org/conda-forge/noarch/packaging-21.3-pyhd8ed1ab_0.tar.bz2#71f1ab2de48613876becddd496371c85
https://conda.anaconda.org/conda-forge/noarch/partd-1.2.0-pyhd8ed1ab_0.tar.bz2#0c32f563d7f22e3a34c95cad8cc95651
https://conda.anaconda.org/conda-forge/win-64/pluggy-1.0.0-py310h5588dad_2.tar.bz2#450125d98e2118237d0154922b7629da
-https://conda.anaconda.org/conda-forge/win-64/pyrsistent-0.18.0-py310he2412df_0.tar.bz2#2f03eda695d46de8bcd06040a3c581d5
+https://conda.anaconda.org/conda-forge/win-64/pyrsistent-0.18.1-py310he2412df_0.tar.bz2#8d9847ea613b97a7d32cb77a3c8f100d
https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.8.2-pyhd8ed1ab_0.tar.bz2#dd999d1cc9f79e67dbb855c8924c7984
https://conda.anaconda.org/conda-forge/win-64/pywin32-303-py310he2412df_0.tar.bz2#e034f495e69b266fc082c92f75398f22
-https://conda.anaconda.org/conda-forge/win-64/pywinpty-1.1.6-py310h00ffb61_0.tar.bz2#30c0f295449652ac5da226bb09c0713c
+https://conda.anaconda.org/conda-forge/win-64/pywinpty-2.0.2-py310h00ffb61_0.tar.bz2#1f92491627fb709352994d13c247cb02
https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0-py310he2412df_3.tar.bz2#e9d0119d704abafde3ed6a0df854fc92
https://conda.anaconda.org/conda-forge/win-64/pyzmq-22.3.0-py310h73ada01_1.tar.bz2#43b030d14cfe99e90d1818e8121da170
-https://conda.anaconda.org/conda-forge/win-64/setuptools-59.8.0-py310h5588dad_0.tar.bz2#4c109fde68a2cf21af07c14bfc70538c
+https://conda.anaconda.org/conda-forge/win-64/setuptools-60.9.2-py310h5588dad_0.tar.bz2#996ca846538740a34dbb5481a06c3d16
https://conda.anaconda.org/conda-forge/win-64/sniffio-1.2.0-py310h5588dad_2.tar.bz2#b2660bc27428af5519ac2b6df9d2cb05
-https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.8.0-pyha770c72_0.tar.bz2#3a38c81fc51f9d709e06719b7376e5f9
+https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.9.2-pyha770c72_0.tar.bz2#465606af3f79bb3d5b4654f4d02c7417
https://conda.anaconda.org/conda-forge/win-64/tornado-6.1-py310he2412df_2.tar.bz2#2e647cb68c37732867dee12dab2d36e2
-https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.0.1-hd8ed1ab_0.tar.bz2#c0d4ec4bcbceb927bff1103a997410d3
+https://conda.anaconda.org/conda-forge/win-64/typed-ast-1.5.2-py310he2412df_0.tar.bz2#a95a934729dd208d6d66ca5a320ed4d1
+https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.1.1-hd8ed1ab_0.tar.bz2#9d1b5bbaa13bca480c80893a87a52c4f
+https://conda.anaconda.org/conda-forge/win-64/unicodedata2-14.0.0-py310he2412df_0.tar.bz2#43bc9f3f429a272a7368d40d98763330
https://conda.anaconda.org/conda-forge/win-64/websockets-10.1-py310he2412df_0.tar.bz2#56b8c1fa4abf27adf85754f01545a55f
https://conda.anaconda.org/conda-forge/win-64/win_inet_pton-1.1.0-py310h5588dad_3.tar.bz2#076952825d5d17d31200c148f03ebe2b
https://conda.anaconda.org/conda-forge/win-64/wsproto-1.0.0-py310h5588dad_2.tar.bz2#bf87b3ffd2f11e98aa6ffca18a9f8ef7
https://conda.anaconda.org/conda-forge/win-64/anyio-3.5.0-py310h5588dad_0.tar.bz2#d86b34b166a34530673809dc9a738303
https://conda.anaconda.org/conda-forge/win-64/argon2-cffi-bindings-21.2.0-py310he2412df_1.tar.bz2#65980510504e1aeb127decee2217fa77
https://conda.anaconda.org/conda-forge/noarch/backports.functools_lru_cache-1.6.4-pyhd8ed1ab_0.tar.bz2#c5b3edc62d6309088f4970b3eaaa65a6
+https://conda.anaconda.org/conda-forge/noarch/black-22.1.0-pyhd8ed1ab_0.tar.bz2#fbc4b306c6ba24c244ffb22a910424da
https://conda.anaconda.org/conda-forge/noarch/bleach-4.1.0-pyhd8ed1ab_0.tar.bz2#4a2104c7b22c222bd0fe03aaef12862c
https://conda.anaconda.org/conda-forge/win-64/brotlipy-0.7.0-py310he2412df_1003.tar.bz2#87576e9f785509ab8c09f2b87c7035ab
https://conda.anaconda.org/conda-forge/win-64/cfitsio-4.0.0-hd67004f_0.tar.bz2#637c0b5964878c8f438750a8d8c1bf7c
https://conda.anaconda.org/conda-forge/noarch/click-default-group-1.2.2-pyhd8ed1ab_1.tar.bz2#72a46ffc25701c173932fd55cf0965d3
https://conda.anaconda.org/conda-forge/win-64/cryptography-36.0.1-py310ha857299_0.tar.bz2#98b944590e40d3252242b4c3f853f784
-https://conda.anaconda.org/conda-forge/noarch/dask-core-2022.1.0-pyhd8ed1ab_0.tar.bz2#e7d934ff2c617f0bfc62ab77c160f093
+https://conda.anaconda.org/conda-forge/noarch/dask-core-2022.2.0-pyhd8ed1ab_0.tar.bz2#d8410c1ab31b3f5196e0cff0125779c1
https://conda.anaconda.org/conda-forge/noarch/deprecation-2.1.0-pyh9f0ad1d_0.tar.bz2#7b6747d7cc2076341029cff659669e8b
-https://conda.anaconda.org/conda-forge/win-64/fonttools-4.28.5-py310he2412df_0.tar.bz2#e8da4e483d92b54769a34b36dc94573c
+https://conda.anaconda.org/conda-forge/win-64/fonttools-4.29.1-py310he2412df_0.tar.bz2#305cff3af19f7710275fda6719b9a89d
https://conda.anaconda.org/conda-forge/noarch/jinja2-3.0.3-pyhd8ed1ab_0.tar.bz2#036d872c653780cb26e797e2e2f61b4c
https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.4.0-pyhd8ed1ab_0.tar.bz2#17ec41acce882e5db4efdcc4c01ca7e0
-https://conda.anaconda.org/conda-forge/win-64/jupyter_core-4.9.1-py310h5588dad_1.tar.bz2#350238bf565fe82efb4b5f401efcc2ed
+https://conda.anaconda.org/conda-forge/win-64/jupyter_core-4.9.2-py310h5588dad_0.tar.bz2#cfaab1787333d1d4bd12770562e36c7b
https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.3.0-pyhd8ed1ab_0.tar.bz2#adf9129f76ca651869acf2219fac2050
-https://conda.anaconda.org/conda-forge/win-64/numpy-1.22.0-py310hcae7c84_0.tar.bz2#a6bac75208a83474372f9167c735a9f2
-https://conda.anaconda.org/conda-forge/win-64/pillow-8.4.0-py310h22f3323_0.tar.bz2#cf0cd830f6846cbdda770a67766adc37
-https://conda.anaconda.org/conda-forge/noarch/pip-21.3.1-pyhd8ed1ab_0.tar.bz2#e4fe2a9af78ff11f1aced7e62128c6a8
+https://conda.anaconda.org/conda-forge/win-64/numpy-1.22.2-py310hcae7c84_0.tar.bz2#a59065c55cab7fd8f65a380f49da597f
+https://conda.anaconda.org/conda-forge/win-64/pillow-9.0.1-py310h767b3fd_1.tar.bz2#b5392084d31948d54853d5de8959abfd
+https://conda.anaconda.org/conda-forge/noarch/pip-22.0.3-pyhd8ed1ab_0.tar.bz2#45dedae69a0ea21cb8566d04b2ca5536
https://conda.anaconda.org/conda-forge/win-64/pydantic-1.9.0-py310he2412df_0.tar.bz2#89b3da947b5f35bb334e933d87090334
https://conda.anaconda.org/conda-forge/noarch/pygments-2.11.2-pyhd8ed1ab_0.tar.bz2#caef60540e2239e27bf62569a5015e3b
https://conda.anaconda.org/conda-forge/win-64/pysocks-1.7.1-py310h5588dad_4.tar.bz2#1a6a04ff640bddc94b0db9294ea5de01
-https://conda.anaconda.org/conda-forge/win-64/pytest-6.2.5-py310h5588dad_2.tar.bz2#23084d3d1f1fe3711e34e962b0060a6b
-https://conda.anaconda.org/conda-forge/win-64/python-chromedriver-binary-96.0.4664.45.0-py310h5588dad_0.tar.bz2#ec50daa932d64a5ef67c77d32617939c
-https://conda.anaconda.org/conda-forge/win-64/terminado-0.12.1-py310h5588dad_1.tar.bz2#63d16b9859fdea18195b04b1a642fb55
+https://conda.anaconda.org/conda-forge/win-64/pytest-7.0.1-py310h5588dad_0.tar.bz2#74b98fe688cf23a8db11de7225c71afa
+https://conda.anaconda.org/conda-forge/win-64/python-chromedriver-binary-98.0.4758.80.0-py310h5588dad_0.tar.bz2#b7743e5838d3c5723dc1be77f74036f6
+https://conda.anaconda.org/conda-forge/noarch/stack_data-0.2.0-pyhd8ed1ab_0.tar.bz2#8c0ce3e6bf18a0c810125aef58a2a6f3
+https://conda.anaconda.org/conda-forge/win-64/terminado-0.13.1-py310h5588dad_0.tar.bz2#0cdf91b8a8a7794cb94fae96d6e8295c
https://conda.anaconda.org/conda-forge/win-64/trio-0.19.0-py310h5588dad_1.tar.bz2#2dc4914af425bcda0b45037f171965bb
https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-21.3.0-pyhd8ed1ab_0.tar.bz2#a0b402db58f73aaab8ee0ca1025a362e
-https://conda.anaconda.org/conda-forge/win-64/imagecodecs-2021.11.20-py310h94038b5_1.tar.bz2#bc4cdf06c48083a7e7fa87509de14e9a
-https://conda.anaconda.org/conda-forge/noarch/imageio-2.13.5-pyh239f2a4_0.tar.bz2#68b42c3670093b73a058b4a795a632c0
+https://conda.anaconda.org/conda-forge/win-64/imagecodecs-2021.11.20-py310h7704284_2.tar.bz2#ddd3f2fed2b51d9bed4568eb847a7137
+https://conda.anaconda.org/conda-forge/noarch/imageio-2.16.0-pyhcf75d05_0.tar.bz2#2f66b44c933bb86e90e1803716658f21
https://conda.anaconda.org/conda-forge/noarch/jupyter-packaging-0.11.1-pyhd8ed1ab_0.tar.bz2#26db422f83443689cba3af9ae43caf65
-https://conda.anaconda.org/conda-forge/noarch/jupyter_client-7.1.1-pyhd8ed1ab_0.tar.bz2#531f22e91137a4812cb0c864b87fa54e
+https://conda.anaconda.org/conda-forge/noarch/jupyter_client-7.1.2-pyhd8ed1ab_0.tar.bz2#9a332b6f8f05629435ce59032df8cfa9
https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.1.2-pyh9f0ad1d_0.tar.bz2#2cbd910890bb328e8959246a1e16fac7
https://conda.anaconda.org/conda-forge/win-64/matplotlib-base-3.5.1-py310h79a7439_0.tar.bz2#87c0a748e6e15c4d3bea14147851f02b
https://conda.anaconda.org/conda-forge/noarch/nbformat-5.1.3-pyhd8ed1ab_0.tar.bz2#bafa5df6d4f8db69a4d197b4657127e7
-https://conda.anaconda.org/conda-forge/win-64/pandas-1.3.5-py310hf5e1058_0.tar.bz2#d4b5ef00ec931c47dbabe6d329a05531
-https://conda.anaconda.org/conda-forge/noarch/pyopenssl-21.0.0-pyhd8ed1ab_0.tar.bz2#8c49efecb7dca466e18b06015e8c88ce
+https://conda.anaconda.org/conda-forge/win-64/pandas-1.4.1-py310hf5e1058_0.tar.bz2#e6d11c7ddad24c7679748b94186e5c66
+https://conda.anaconda.org/conda-forge/noarch/pyopenssl-22.0.0-pyhd8ed1ab_0.tar.bz2#1d7e241dfaf5475e893d4b824bb71b44
https://conda.anaconda.org/conda-forge/noarch/pytest-cov-3.0.0-pyhd8ed1ab_0.tar.bz2#0f7cac11bb696b62d378bde725bfc3eb
https://conda.anaconda.org/conda-forge/win-64/pywavelets-1.2.0-py310h2873277_1.tar.bz2#b3842340538035083e79730dcbf698d2
-https://conda.anaconda.org/conda-forge/win-64/scipy-1.7.3-py310h33db832_0.tar.bz2#ebf83a246b208fb8706afd96705653f4
+https://conda.anaconda.org/conda-forge/win-64/scipy-1.8.0-py310h33db832_1.tar.bz2#94aed18abea504ed1053fed14887bd53
https://conda.anaconda.org/conda-forge/noarch/trio-websocket-0.9.2-pyhd8ed1ab_0.tar.bz2#7acf3fc982eef23b24aed10e80d5dc99
https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.5-pyh9f0ad1d_2.tar.bz2#5266fcd697043c59621fda522b3d78ee
-https://conda.anaconda.org/conda-forge/noarch/jupytext-1.13.6-pyheef035f_0.tar.bz2#686b0fb2e2183e96ebb8faadbb491652
-https://conda.anaconda.org/conda-forge/noarch/nbclient-0.5.10-pyhd8ed1ab_1.tar.bz2#000cfe33d6fa746c5a9d1b29a79342a3
+https://conda.anaconda.org/conda-forge/noarch/jupytext-1.13.7-pyhd0ecf6b_0.tar.bz2#6e8deee97d83b8f99d34be2db26ccc6e
+https://conda.anaconda.org/conda-forge/noarch/nbclient-0.5.11-pyhd8ed1ab_0.tar.bz2#5f613d000dca7369b6309f0a3995f08b
https://conda.anaconda.org/conda-forge/noarch/networkx-2.6.3-pyhd8ed1ab_1.tar.bz2#4028eff5a1d3deed58c98774e7aa9891
-https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.24-pyha770c72_0.tar.bz2#edaf527a6d394e2b965aff228c2e552f
-https://conda.anaconda.org/conda-forge/noarch/tifffile-2021.11.2-pyhd8ed1ab_0.tar.bz2#b07562132d44945a4ae301f75cf731b1
+https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.27-pyha770c72_0.tar.bz2#6c23d05d56ee3a82b57af972a8509888
+https://conda.anaconda.org/conda-forge/noarch/tifffile-2022.2.9-pyhd8ed1ab_0.tar.bz2#619b7bd585e11ae063c72e2cf9880145
https://conda.anaconda.org/conda-forge/noarch/urllib3-1.26.8-pyhd8ed1ab_1.tar.bz2#53f1387c68c21cecb386e2cde51b3f7c
https://conda.anaconda.org/conda-forge/noarch/vega_datasets-0.9.0-pyhd3deb0d_0.tar.bz2#c754e69d9d5de4a69ff0972318349bd0
-https://conda.anaconda.org/conda-forge/win-64/ipython-7.31.0-py310h5588dad_0.tar.bz2#8ed2acac70603e492b0a22b83982907b
-https://conda.anaconda.org/conda-forge/win-64/nbconvert-6.4.0-py310h5588dad_0.tar.bz2#0e1f6f618f497c381746760ffbf26ba7
+https://conda.anaconda.org/conda-forge/win-64/ipython-8.0.1-py310h5588dad_2.tar.bz2#f891ab0617770f29b8b9bce7ef793d0b
+https://conda.anaconda.org/conda-forge/win-64/nbconvert-6.4.2-py310h5588dad_0.tar.bz2#8b61a7dc5b50ca890d1379f90fe7474b
https://conda.anaconda.org/conda-forge/noarch/requests-2.27.1-pyhd8ed1ab_0.tar.bz2#7c1c427246b057b8fa97200ecdb2ed62
https://conda.anaconda.org/conda-forge/win-64/scikit-image-0.19.1-py310hf5e1058_0.tar.bz2#99f76f01fe093ef88470455d4d89d09a
https://conda.anaconda.org/conda-forge/noarch/selenium-4.1.0-pyhd8ed1ab_0.tar.bz2#54d6ec87313ba485cc787b81bf4142c0
https://conda.anaconda.org/conda-forge/noarch/ensureconda-1.4.1-pyhd8ed1ab_0.tar.bz2#52a7f7cc9076e2c8a25e15e19ad42821
-https://conda.anaconda.org/conda-forge/win-64/ipykernel-6.7.0-py310hbbfc1a7_0.tar.bz2#f205d5d215881d298ef9e06edd8ec291
-https://conda.anaconda.org/conda-forge/noarch/jupyter_server-1.13.3-pyhd8ed1ab_0.tar.bz2#0701d62fbbd51356ac3212f401ad18b6
+https://conda.anaconda.org/conda-forge/win-64/ipykernel-6.9.1-py310hbbfc1a7_0.tar.bz2#b6cef5777da0d3b02807a240eda61fb1
+https://conda.anaconda.org/conda-forge/noarch/jupyter_server-1.13.5-pyhd8ed1ab_0.tar.bz2#de2bff665fa83862dbaa9d85720e7719
https://conda.anaconda.org/conda-forge/noarch/conda-lock-0.13.2-pyhd8ed1ab_0.tar.bz2#4fa525b4257e4938eaeb2269fdf33d1f
https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.10.3-pyhd8ed1ab_0.tar.bz2#65d62a616bed5237b85b9e2a88378ec0
https://conda.anaconda.org/conda-forge/noarch/nbval-0.9.6-pyh9f0ad1d_0.tar.bz2#b627e05284e7affc46b6e4878aa1d96b
-https://conda.anaconda.org/conda-forge/noarch/notebook-6.4.7-pyha770c72_0.tar.bz2#8bf6e732d753521a06571d1efe8e58f2
-https://conda.anaconda.org/conda-forge/noarch/voila-0.3.0-pyhd8ed1ab_1.tar.bz2#5093d08d3afcc35b5ab8184f3b19c028
+https://conda.anaconda.org/conda-forge/noarch/notebook-6.4.8-pyha770c72_0.tar.bz2#87d880386179ed952a9d656faf2ff860
+https://conda.anaconda.org/conda-forge/noarch/voila-0.3.1-pyhd8ed1ab_0.tar.bz2#57d04a687ad1b72d30159a1b2bbf8f3e
https://conda.anaconda.org/conda-forge/noarch/nbclassic-0.3.5-pyhd8ed1ab_0.tar.bz2#e9e2281b7dc08d876fc789af0f571ade
-https://conda.anaconda.org/conda-forge/noarch/jupyterlab-3.2.8-pyhd8ed1ab_0.tar.bz2#e2cc0671571f14af58f832c912dee025
+https://conda.anaconda.org/conda-forge/noarch/jupyterlab-3.2.9-pyhd8ed1ab_0.tar.bz2#610d3fc318b6eb159483151070b301f4
diff --git a/python/vegafusion/setup.cfg b/python/vegafusion/setup.cfg
index 8af15d2b0..e4e0a340b 100644
--- a/python/vegafusion/setup.cfg
+++ b/python/vegafusion/setup.cfg
@@ -31,6 +31,7 @@ install_requires =
altair>=4.2.0
pyarrow>=6
pandas
+ psutil
[options.extras_require]
embed = vegafusion-python-embed
diff --git a/python/vegafusion/vegafusion/runtime.py b/python/vegafusion/vegafusion/runtime.py
index fe9d92acd..6a65e625f 100644
--- a/python/vegafusion/vegafusion/runtime.py
+++ b/python/vegafusion/vegafusion/runtime.py
@@ -15,12 +15,14 @@
# along with this program. If not, see .
import multiprocessing
+import psutil
class VegaFusionRuntime:
- def __init__(self, cache_capacity, worker_threads):
+ def __init__(self, cache_capacity, memory_limit, worker_threads):
self._runtime = None
self._cache_capacity = cache_capacity
+ self._memory_limit = memory_limit
self._worker_threads = worker_threads
@property
@@ -29,7 +31,7 @@ def runtime(self):
# Try to initialize an embedded runtime
from vegafusion_embed import PyTaskGraphRuntime
- self._runtime = PyTaskGraphRuntime(self.cache_capacity, self.worker_threads)
+ self._runtime = PyTaskGraphRuntime(self.cache_capacity, self.memory_limit, self.worker_threads)
return self._runtime
def process_request_bytes(self, request):
@@ -50,6 +52,49 @@ def worker_threads(self, value):
self._worker_threads = value
self.reset()
+ @property
+ def total_memory(self):
+ if self._runtime:
+ return self._runtime.total_memory()
+ else:
+ return None
+
+ @property
+ def _protected_memory(self):
+ if self._runtime:
+ return self._runtime.protected_memory()
+ else:
+ return None
+
+ @property
+ def _probationary_memory(self):
+ if self._runtime:
+ return self._runtime.probationary_memory()
+ else:
+ return None
+
+ @property
+ def size(self):
+ if self._runtime:
+ return self._runtime.size()
+ else:
+ return None
+
+ @property
+ def memory_limit(self):
+ return self._memory_limit
+
+ @memory_limit.setter
+ def memory_limit(self, value):
+ """
+ Restart the runtime with the specified memory limit
+
+ :param threads: Max approximate memory usage of cache
+ """
+ if value != self._memory_limit:
+ self._memory_limit = value
+ self.reset()
+
@property
def cache_capacity(self):
return self._cache_capacity
@@ -78,4 +123,4 @@ def __repr__(self):
)
-runtime = VegaFusionRuntime(16, multiprocessing.cpu_count())
+runtime = VegaFusionRuntime(64, psutil.virtual_memory().total // 2, psutil.cpu_count())
diff --git a/vegafusion-core/src/task_graph/memory.rs b/vegafusion-core/src/task_graph/memory.rs
new file mode 100644
index 000000000..a1d1024ec
--- /dev/null
+++ b/vegafusion-core/src/task_graph/memory.rs
@@ -0,0 +1,95 @@
+use crate::data::scalar::ScalarValue;
+use crate::data::table::VegaFusionTable;
+use arrow::array::ArrayRef;
+use arrow::datatypes::{DataType, Field, Schema};
+use arrow::record_batch::RecordBatch;
+use std::mem::{size_of, size_of_val};
+
+/// Get the size of a Field value, including any inner heap-allocated data
+fn size_of_field(field: &Field) -> usize {
+ size_of::() + inner_size_of_dtype(field.data_type())
+}
+
+/// Get the size of inner heap-allocated data associated with a DataType value
+fn inner_size_of_dtype(value: &DataType) -> usize {
+ match value {
+ DataType::Map(field, _) => size_of_field(field),
+ DataType::Timestamp(_, Some(tz)) => size_of::() + size_of_val(tz.as_bytes()),
+ DataType::List(field) => size_of_field(field),
+ DataType::LargeList(field) => size_of_field(field),
+ DataType::FixedSizeList(field, _) => size_of_field(field),
+ DataType::Struct(fields) => {
+ size_of::>() + fields.iter().map(size_of_field).sum::()
+ }
+ DataType::Union(fields) => {
+ size_of::>() + fields.iter().map(size_of_field).sum::()
+ }
+ DataType::Dictionary(key_dtype, value_dtype) => {
+ 2 * size_of::()
+ + inner_size_of_dtype(key_dtype)
+ + inner_size_of_dtype(value_dtype)
+ }
+ _ => {
+ // No inner heap-allocated data
+ 0
+ }
+ }
+}
+
+/// Get the size of inner heap-allocated data associated with a ScalarValue value
+pub fn inner_size_of_scalar(value: &ScalarValue) -> usize {
+ match value {
+ ScalarValue::Utf8(Some(s)) => size_of_val(s.as_bytes()) + size_of::(),
+ ScalarValue::LargeUtf8(Some(s)) => size_of_val(s.as_bytes()) + size_of::(),
+ ScalarValue::Binary(Some(b)) => size_of_val(b.as_slice()) + size_of::>(),
+ ScalarValue::LargeBinary(Some(b)) => size_of_val(b.as_slice()) + size_of::>(),
+ ScalarValue::List(Some(values), dtype) => {
+ let values_bytes: usize = size_of::>()
+ + values
+ .iter()
+ .map(|v| size_of::() + inner_size_of_scalar(v))
+ .sum::();
+
+ let dtype_bytes = size_of::() + inner_size_of_dtype(dtype);
+
+ values_bytes + dtype_bytes
+ }
+ ScalarValue::Struct(Some(values), fields) => {
+ let values_bytes: usize = size_of::>()
+ + values
+ .iter()
+ .map(|v| size_of::() + inner_size_of_scalar(v))
+ .sum::();
+
+ let fields_bytes: usize =
+ size_of::>() + fields.iter().map(size_of_field).sum::();
+
+ values_bytes + fields_bytes
+ }
+ _ => {
+ // No inner heap-allocated data
+ 0
+ }
+ }
+}
+
+pub fn size_of_array_ref(array: &ArrayRef) -> usize {
+ array.get_array_memory_size() + inner_size_of_dtype(array.data_type()) + size_of::()
+}
+
+pub fn size_of_schema(schema: &Schema) -> usize {
+ size_of::() + schema.fields().iter().map(size_of_field).sum::()
+}
+
+pub fn size_of_record_batch(batch: &RecordBatch) -> usize {
+ let schema = batch.schema();
+ let schema_size: usize = size_of_schema(schema.as_ref());
+ let arrays_size: usize = batch.columns().iter().map(size_of_array_ref).sum();
+ size_of::() + schema_size + arrays_size
+}
+
+pub fn inner_size_of_table(value: &VegaFusionTable) -> usize {
+ let schema_size: usize = size_of_schema(&value.schema);
+ let size_of_batches: usize = value.batches.iter().map(size_of_record_batch).sum();
+ schema_size + size_of_batches
+}
diff --git a/vegafusion-core/src/task_graph/mod.rs b/vegafusion-core/src/task_graph/mod.rs
index 22d4ffe69..da5ddb31a 100644
--- a/vegafusion-core/src/task_graph/mod.rs
+++ b/vegafusion-core/src/task_graph/mod.rs
@@ -17,6 +17,7 @@
* If not, see http://www.gnu.org/licenses/.
*/
pub mod graph;
+pub mod memory;
pub mod scope;
pub mod task;
pub mod task_value;
diff --git a/vegafusion-core/src/task_graph/task_value.rs b/vegafusion-core/src/task_graph/task_value.rs
index 57528d5d2..eb61e7fdc 100644
--- a/vegafusion-core/src/task_graph/task_value.rs
+++ b/vegafusion-core/src/task_graph/task_value.rs
@@ -21,6 +21,7 @@ use crate::data::table::VegaFusionTable;
use crate::error::{Result, VegaFusionError};
use crate::proto::gen::tasks::task_value::Data;
use crate::proto::gen::tasks::TaskValue as ProtoTaskValue;
+use crate::task_graph::memory::{inner_size_of_scalar, inner_size_of_table};
use arrow::record_batch::RecordBatch;
use serde_json::Value;
use std::convert::TryFrom;
@@ -52,6 +53,15 @@ impl TaskValue {
TaskValue::Table(value) => Ok(value.to_json()),
}
}
+
+ pub fn size_of(&self) -> usize {
+ let inner_size = match self {
+ TaskValue::Scalar(scalar) => inner_size_of_scalar(scalar),
+ TaskValue::Table(table) => inner_size_of_table(table),
+ };
+
+ std::mem::size_of::() + inner_size
+ }
}
impl TryFrom<&ProtoTaskValue> for TaskValue {
diff --git a/vegafusion-python-embed/src/lib.rs b/vegafusion-python-embed/src/lib.rs
index ef99b36a1..13c325c7a 100644
--- a/vegafusion-python-embed/src/lib.rs
+++ b/vegafusion-python-embed/src/lib.rs
@@ -32,7 +32,11 @@ struct PyTaskGraphRuntime {
#[pymethods]
impl PyTaskGraphRuntime {
#[new]
- pub fn new(max_capacity: i32, worker_threads: Option) -> PyResult {
+ pub fn new(
+ max_capacity: Option,
+ memory_limit: Option,
+ worker_threads: Option,
+ ) -> PyResult {
let mut tokio_runtime_builder = tokio::runtime::Builder::new_multi_thread();
tokio_runtime_builder.enable_all();
@@ -46,7 +50,7 @@ impl PyTaskGraphRuntime {
.external("Failed to create Tokio thread pool")?;
Ok(Self {
- runtime: TaskGraphRuntime::new(max_capacity as usize),
+ runtime: TaskGraphRuntime::new(max_capacity, memory_limit),
tokio_runtime,
})
}
@@ -61,6 +65,22 @@ impl PyTaskGraphRuntime {
pub fn clear_cache(&self) {
self.tokio_runtime.block_on(self.runtime.clear_cache());
}
+
+ pub fn size(&self) -> usize {
+ self.runtime.cache.size()
+ }
+
+ pub fn total_memory(&self) -> usize {
+ self.runtime.cache.total_memory()
+ }
+
+ pub fn protected_memory(&self) -> usize {
+ self.runtime.cache.protected_memory()
+ }
+
+ pub fn probationary_memory(&self) -> usize {
+ self.runtime.cache.probationary_memory()
+ }
}
/// A Python module implemented in Rust. The name of this function must match
diff --git a/vegafusion-rt-datafusion/src/task_graph/cache.rs b/vegafusion-rt-datafusion/src/task_graph/cache.rs
index 922b50f16..3adf294f1 100644
--- a/vegafusion-rt-datafusion/src/task_graph/cache.rs
+++ b/vegafusion-rt-datafusion/src/task_graph/cache.rs
@@ -16,56 +16,254 @@
* License along with this program.
* If not, see http://www.gnu.org/licenses/.
*/
-use async_lock::{Mutex, RwLock};
+use async_lock::{Mutex, MutexGuard, RwLock};
use futures::FutureExt;
use lru::LruCache;
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::future::Future;
use std::panic::{resume_unwind, AssertUnwindSafe};
+use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
+use std::time::Instant;
use vegafusion_core::error::{DuplicateResult, Result, ToExternalError};
use vegafusion_core::task_graph::task_value::TaskValue;
#[derive(Debug, Clone)]
struct CachedValue {
- value: NodeValue, // Maybe add metrics like compute time, or a cache weight
+ value: NodeValue,
+ calculation_millis: u128,
+}
+
+impl CachedValue {
+ pub fn size_of(&self) -> usize {
+ self.value.0.size_of() + self.value.1.iter().map(|v| v.size_of()).sum::()
+ }
}
type NodeValue = (TaskValue, Vec);
type Initializer = Arc>>>;
+/// The VegaFusionCache uses a Segmented LRU (SLRU) cache policy
+/// (https://en.wikipedia.org/wiki/Cache_replacement_policies#Segmented_LRU_(SLRU)) where both the
+/// protected and probationary LRU caches are limited by capacity (number of entries) and memory
+/// limit.
#[derive(Debug, Clone)]
pub struct VegaFusionCache {
- values: Arc>>,
+ protected_cache: Arc>>,
+ probationary_cache: Arc>>,
+ protected_fraction: f64,
initializers: Arc>>,
+ size: Arc,
+ protected_memory: Arc,
+ probationary_memory: Arc,
+ capacity: Option,
+ memory_limit: Option,
}
impl VegaFusionCache {
- pub fn new(capacity: usize) -> Self {
+ pub fn new(capacity: Option, size_limit: Option) -> Self {
Self {
- values: Arc::new(Mutex::new(LruCache::new(capacity))),
+ protected_cache: Arc::new(Mutex::new(LruCache::unbounded())),
+ probationary_cache: Arc::new(Mutex::new(LruCache::unbounded())),
+ protected_fraction: 0.5,
initializers: Default::default(),
+ capacity,
+ memory_limit: size_limit,
+ size: Arc::new(AtomicUsize::new(0)),
+ protected_memory: Arc::new(AtomicUsize::new(0)),
+ probationary_memory: Arc::new(AtomicUsize::new(0)),
}
}
+ pub fn capacity(&self) -> Option {
+ self.capacity
+ }
+
+ pub fn memory_limit(&self) -> Option {
+ self.memory_limit
+ }
+
+ pub fn size(&self) -> usize {
+ self.size.load(Ordering::Relaxed)
+ }
+
+ pub fn total_memory(&self) -> usize {
+ self.protected_memory() + self.probationary_memory()
+ }
+
+ pub fn protected_memory(&self) -> usize {
+ self.protected_memory.load(Ordering::Relaxed)
+ }
+
+ pub fn probationary_memory(&self) -> usize {
+ self.probationary_memory.load(Ordering::Relaxed)
+ }
+
pub async fn clear(&self) {
// Clear the values cache. There may still be initializers representing in progress
// futures which will not be cleared.
- self.values.lock().await.clear();
+ self.protected_cache.lock().await.clear();
+ self.probationary_cache.lock().await.clear();
+ self.protected_memory.store(0, Ordering::Relaxed);
+ self.probationary_memory.store(0, Ordering::Relaxed);
+ self.size.store(0, Ordering::Relaxed);
+ }
+
+ async fn get(&self, state_fingerprint: u64) -> Option {
+ let mut protected = self.protected_cache.lock().await;
+ let mut probationary = self.probationary_cache.lock().await;
+
+ if protected.contains(&state_fingerprint) {
+ protected.get(&state_fingerprint).cloned()
+ } else if probationary.contains(&state_fingerprint) {
+ // Promote entry from probationary to protected
+ let value = probationary.pop(&state_fingerprint).unwrap();
+ let value_memory = value.size_of();
+ protected.put(state_fingerprint, value.clone());
+
+ self.protected_memory
+ .fetch_add(value_memory, Ordering::Relaxed);
+ self.probationary_memory
+ .fetch_sub(value_memory, Ordering::Relaxed);
+
+ // Balance caches
+ self.balance(&mut protected, &mut probationary);
+
+ Some(value)
+ } else {
+ None
+ }
+ }
+
+ fn pop_protected_lru(
+ &self,
+ protected: &mut MutexGuard>,
+ probationary: &mut MutexGuard>,
+ ) {
+ // Remove one protected LRU entry
+ let (key, popped_value) = protected.pop_lru().unwrap();
+ let popped_memory = popped_value.size_of();
+
+ // Decrement protected memory
+ self.protected_memory
+ .fetch_sub(popped_memory, Ordering::Relaxed);
+
+ // Add entry to probationary cache
+ probationary.put(key, popped_value);
+
+ // Increment probationary memory
+ self.probationary_memory
+ .fetch_add(popped_memory, Ordering::Relaxed);
}
- async fn get_from_values(&self, state_fingerprint: u64) -> Option {
- // This is a write lock because the LruCache.get function mutates the Cache to update
- // the LRU status
- self.values.lock().await.get(&state_fingerprint).cloned()
+ fn pop_probationary_lru(&self, probationary: &mut MutexGuard>) {
+ let (_, popped_value) = probationary.pop_lru().unwrap();
+ let popped_memory = popped_value.size_of();
+
+ // Decrement protected memory
+ self.probationary_memory
+ .fetch_sub(popped_memory, Ordering::Relaxed);
}
- async fn set_value(&self, state_fingerprint: u64, value: NodeValue) -> Option {
- self.values
- .lock()
- .await
- .put(state_fingerprint, CachedValue { value })
+ fn balance(
+ &self,
+ protected: &mut MutexGuard>,
+ probationary: &mut MutexGuard>,
+ ) {
+ // Compute capacity and memory limits for both protected and probationary caches
+ let (protected_capacity, probationary_capacity) = if let Some(capacity) = self.capacity {
+ let protected_capacity = (capacity as f64 * self.protected_fraction).ceil() as usize;
+ (
+ Some(protected_capacity),
+ Some(capacity - protected_capacity),
+ )
+ } else {
+ (None, None)
+ };
+
+ let (protected_mem_limit, probationary_mem_limit) =
+ if let Some(memory_limit) = self.memory_limit {
+ let protected_mem_limit =
+ (memory_limit as f64 * self.protected_fraction).ceil() as usize;
+ (
+ Some(protected_mem_limit),
+ Some(memory_limit - protected_mem_limit),
+ )
+ } else {
+ (None, None)
+ };
+
+ // Step 1: Shrink protected cache until it satisfies limits, moving evicted items to
+ // probationary cache
+ // Pop to capacity limit
+ if let Some(capacity) = protected_capacity {
+ while protected.len() > 1 && protected.len() > capacity {
+ self.pop_protected_lru(protected, probationary);
+ }
+ }
+
+ // Pop LRU to memory limit
+ if let Some(memory_limit) = protected_mem_limit {
+ while protected.len() > 1
+ && self.protected_memory.load(Ordering::Relaxed) > memory_limit
+ {
+ self.pop_protected_lru(protected, probationary);
+ }
+ }
+
+ // Step 2: Shrink probationary cache until it satisfies limits,
+ // decrementing memory estimate
+ if let Some(capacity) = probationary_capacity {
+ while probationary.len() > 1 && probationary.len() > capacity {
+ self.pop_probationary_lru(probationary);
+ }
+ }
+
+ // Pop LRU to memory limit
+ if let Some(memory_limit) = probationary_mem_limit {
+ while probationary.len() > 1
+ && self.probationary_memory.load(Ordering::Relaxed) > memory_limit
+ {
+ self.pop_probationary_lru(probationary);
+ }
+ }
+
+ // Step 3: Update size atomics
+ self.size
+ .store(protected.len() + probationary.len(), Ordering::Relaxed);
+ }
+
+ async fn set_value(&self, state_fingerprint: u64, value: NodeValue, calculation_millis: u128) {
+ let cache_value = CachedValue {
+ value,
+ calculation_millis,
+ };
+ let value_memory = cache_value.size_of();
+
+ let mut protected = self.protected_cache.lock().await;
+ let mut probationary = self.probationary_cache.lock().await;
+ if protected.contains(&state_fingerprint) {
+ // Set on protected to update usage
+ protected.put(state_fingerprint, cache_value);
+ } else if probationary.contains(&state_fingerprint) {
+ // Promote from probationary to protected
+ protected.put(
+ state_fingerprint,
+ probationary.pop(&state_fingerprint).unwrap(),
+ );
+ self.protected_memory
+ .fetch_add(value_memory, Ordering::Relaxed);
+ self.probationary_memory
+ .fetch_sub(value_memory, Ordering::Relaxed);
+ self.balance(&mut protected, &mut probationary);
+ } else {
+ // Add to probationary and update memory usage
+ probationary.put(state_fingerprint, cache_value);
+ self.probationary_memory
+ .fetch_add(value_memory, Ordering::Relaxed);
+ self.balance(&mut protected, &mut probationary);
+ }
}
async fn remove_initializer(&self, state_fingerprint: u64) -> Option {
@@ -81,7 +279,7 @@ impl VegaFusionCache {
F: Future