From 7a7797a5b9079a0c4fd86260053a9050e8944dfa Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 25 Dec 2022 14:08:12 -0500 Subject: [PATCH] graphics refactor, tested and works --- examples/gridplot.ipynb | 30 ++-- examples/simple.ipynb | 202 +++++++++++++--------- fastplotlib/graphics/_base.py | 61 ++----- fastplotlib/graphics/features/__init__.py | 4 +- fastplotlib/graphics/features/_base.py | 20 ++- fastplotlib/graphics/features/_colors.py | 64 ++++++- fastplotlib/graphics/features/_data.py | 144 +++++++++++---- fastplotlib/graphics/image.py | 50 ++++-- fastplotlib/graphics/line.py | 35 ++-- fastplotlib/graphics/linecollection.py | 3 +- fastplotlib/graphics/scatter.py | 56 +++++- fastplotlib/graphics/text.py | 4 +- fastplotlib/layouts/_subplot.py | 11 +- fastplotlib/utils/functions.py | 33 ---- fastplotlib/widgets/image.py | 8 +- 15 files changed, 476 insertions(+), 249 deletions(-) diff --git a/examples/gridplot.ipynb b/examples/gridplot.ipynb index 2e558fa16..a5b7f4209 100644 --- a/examples/gridplot.ipynb +++ b/examples/gridplot.ipynb @@ -28,7 +28,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "8c47742795824d9e95c8d3b46df08a51", + "model_id": "cae156b0748142ff9335f4612d39e9ed", "version_major": 2, "version_minor": 0 }, @@ -42,7 +42,7 @@ { "data": { "text/html": [ - "
initial snapshot
" + "
initial snapshot
" ], "text/plain": [ "" @@ -54,7 +54,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "3d95102a394d4fbcb4d4bba9fa527089", + "model_id": "03295486e8ba449d80429f53832698e4", "version_major": 2, "version_minor": 0 }, @@ -90,7 +90,7 @@ "grid_plot = GridPlot(\n", " shape=grid_shape,\n", " controllers=controllers,\n", - " names=names\n", + " names=names,\n", ")\n", "\n", "\n", @@ -130,10 +130,10 @@ { "data": { "text/plain": [ - "subplot0: Subplot @ 0x7f418bbcaef0\n", + "subplot0: Subplot @ 0x7fbddc5bf9d0\n", " parent: None\n", " Graphics:\n", - "\t'rand-image' fastplotlib.ImageGraphic @ 0x7f418bbcae90" + "\t'rand-image' fastplotlib.ImageGraphic @ 0x7fbddc5bf970" ] }, "execution_count": 3, @@ -155,10 +155,10 @@ { "data": { "text/plain": [ - "subplot0: Subplot @ 0x7f418bbcaef0\n", + "subplot0: Subplot @ 0x7fbddc5bf9d0\n", " parent: None\n", " Graphics:\n", - "\t'rand-image' fastplotlib.ImageGraphic @ 0x7f418bbcae90" + "\t'rand-image' fastplotlib.ImageGraphic @ 0x7fbddc5bf970" ] }, "execution_count": 4, @@ -189,7 +189,7 @@ { "data": { "text/plain": [ - "'rand-image' fastplotlib.ImageGraphic @ 0x7f418bbcae90" + "'rand-image' fastplotlib.ImageGraphic @ 0x7fbddc5bf970" ] }, "execution_count": 5, @@ -209,7 +209,8 @@ "metadata": {}, "outputs": [], "source": [ - "grid_plot[\"subplot0\"][\"rand-image\"].clim = (0.6, 0.8)" + "grid_plot[\"subplot0\"][\"rand-image\"].vmin = 0.6\n", + "grid_plot[\"subplot0\"][\"rand-image\"].vmax = 0.8" ] }, { @@ -222,17 +223,18 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 9, "id": "2fafe992-4783-40f2-b044-26a2835dd50a", "metadata": {}, "outputs": [], "source": [ - "grid_plot[1, 0][\"rand-image\"].clim = (0.1, 0.3)" + "grid_plot[1, 0][\"rand-image\"].vim = 0.1\n", + "grid_plot[1, 0][\"rand-image\"].vmax = 0.3" ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 10, "id": "a025b76c-77f8-4aeb-ac33-5bb6d0bb5a9a", "metadata": {}, "outputs": [ @@ -242,7 +244,7 @@ "'image'" ] }, - "execution_count": 8, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } diff --git a/examples/simple.ipynb b/examples/simple.ipynb index b1d3b88b5..b64aad56c 100644 --- a/examples/simple.ipynb +++ b/examples/simple.ipynb @@ -41,7 +41,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "d8de4d1574414bfcb186eaa62872c781", + "model_id": "a4fb8c6563f14824971deecd96965972", "version_major": 2, "version_minor": 0 }, @@ -55,7 +55,7 @@ { "data": { "text/html": [ - "
initial snapshot
" + "
initial snapshot
" ], "text/plain": [ "" @@ -67,7 +67,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "5c46808362e840ad9cc50bfa2e5f7346", + "model_id": "95958dc9ae4e4bf18aa1d1f68ac667fb", "version_major": 2, "version_minor": 0 }, @@ -94,6 +94,16 @@ "plot.show()" ] }, + { + "cell_type": "code", + "execution_count": 3, + "id": "de816c88-1c4a-4071-8a5e-c46c93671ef5", + "metadata": {}, + "outputs": [], + "source": [ + "image_graphic.cmap = \"viridis\"" + ] + }, { "cell_type": "markdown", "id": "be5b408f-dd91-4e36-807a-8c22c8d7d216", @@ -112,17 +122,17 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 4, "id": "e6ba689c-ff4a-44ef-9663-f2c8755072c4", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "['random-image' fastplotlib.ImageGraphic @ 0x7fa21d5dd1b0]" + "['random-image' fastplotlib.ImageGraphic @ 0x7f748162fd90]" ] }, - "execution_count": 3, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -133,17 +143,17 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 5, "id": "5b18f4e3-e13b-46d5-af1f-285c5a7fdc12", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "'random-image' fastplotlib.ImageGraphic @ 0x7faf6bd71600" + "'random-image' fastplotlib.ImageGraphic @ 0x7f748162fd90" ] }, - "execution_count": 7, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -162,17 +172,17 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 6, "id": "2b5c1321-1fd4-44bc-9433-7439ad3e22cf", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "'random-image' fastplotlib.ImageGraphic @ 0x7fa21d5dd1b0" + "'random-image' fastplotlib.ImageGraphic @ 0x7f748162fd90" ] }, - "execution_count": 4, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -183,7 +193,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 7, "id": "b12bf75e-4e93-4930-9146-e96324fdf3f6", "metadata": {}, "outputs": [ @@ -193,7 +203,7 @@ "True" ] }, - "execution_count": 5, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -205,7 +215,9 @@ { "cell_type": "markdown", "id": "1cb03f42-1029-4b16-a16b-35447d9e2955", - "metadata": {}, + "metadata": { + "tags": [] + }, "source": [ "### Image updates\n", "\n", @@ -214,14 +226,14 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 8, "id": "aadd757f-6379-4f52-a709-46aa57c56216", "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "f2ccae25614d47cda980e011f17771cb", + "model_id": "ab597f9780064497b7ab0fc8d52dd538", "version_major": 2, "version_minor": 0 }, @@ -235,7 +247,7 @@ { "data": { "text/html": [ - "
initial snapshot
" + "
initial snapshot
" ], "text/plain": [ "" @@ -247,7 +259,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "33ca1052a871445188bbe7d3c8725cf1", + "model_id": "c75d9b5ae24b4c98b865ee7a869d665f", "version_major": 2, "version_minor": 0 }, @@ -255,7 +267,7 @@ "JupyterWgpuCanvas()" ] }, - "execution_count": 6, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -264,6 +276,8 @@ "# create another `Plot` instance\n", "plot_v = Plot()\n", "\n", + "plot.canvas.max_buffered_frames = 1\n", + "\n", "# make some random data again\n", "data = np.random.rand(512, 512)\n", "\n", @@ -295,14 +309,14 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 9, "id": "86e70b1e-4328-4035-b992-70dff16d2a69", "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "f486d69ebedb454ebc8d0fd164b3c04c", + "model_id": "36a85c99623947c9a3ed729b09f6b212", "version_major": 2, "version_minor": 0 }, @@ -316,7 +330,7 @@ { "data": { "text/html": [ - "
initial snapshot
" + "
initial snapshot
" ], "text/plain": [ "" @@ -328,7 +342,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "c275d36864264618af7c9adde7ee5173", + "model_id": "692cfda570284fd0ae43f45530f87885", "version_major": 2, "version_minor": 0 }, @@ -336,7 +350,7 @@ "JupyterWgpuCanvas()" ] }, - "execution_count": 7, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } @@ -377,19 +391,19 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 10, "id": "ef9743b3-5f81-4b79-9502-fa5fca08e56d", "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "4dd8df34e05948638f5191c81735ddc8", + "model_id": "b9561a7e5aec4ad2b42b263c2fbdb87d", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "VBox(children=(JupyterWgpuCanvas(frame_feedback={'index': 819, 'timestamp': 1671971212.7522302, 'localtime': 1…" + "VBox(children=(JupyterWgpuCanvas(frame_feedback={'index': 137, 'timestamp': 1671994231.3569584, 'localtime': 1…" ] }, "metadata": {}, @@ -411,19 +425,19 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 11, "id": "11839d95-8ff7-444c-ae13-6b072c3112c5", "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "a7b4e59995ce420e8b983ae6f9b3d4d8", + "model_id": "70b73156a4dd4710858258eee985ecae", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "HBox(children=(JupyterWgpuCanvas(frame_feedback={'index': 1004, 'timestamp': 1671971220.7120316, 'localtime': …" + "HBox(children=(JupyterWgpuCanvas(frame_feedback={'index': 244, 'timestamp': 1671994235.1249204, 'localtime': 1…" ] }, "metadata": {}, @@ -454,7 +468,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 12, "id": "0bcedf83-cbdd-4ec2-b8d5-172aa72a3e04", "metadata": {}, "outputs": [], @@ -529,7 +543,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 13, "id": "8b560151-c258-415c-a20d-3cccd421f44a", "metadata": {}, "outputs": [ @@ -539,7 +553,7 @@ "(1000, 512, 512)" ] }, - "execution_count": 11, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } @@ -566,14 +580,14 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 14, "id": "62166a9f-ab43-45cc-a6db-6d441387e9a5", "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "f4b20b310229436889a80e9676e36865", + "model_id": "bbc9fc354724480898744eefc88ab995", "version_major": 2, "version_minor": 0 }, @@ -587,7 +601,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "5b1232536af6482e926793142c22aa3b", + "model_id": "0462fb2ba19a42c587111e7652d4343c", "version_major": 2, "version_minor": 0 }, @@ -633,7 +647,9 @@ { "cell_type": "markdown", "id": "e7859338-8162-408b-ac72-37e606057045", - "metadata": {}, + "metadata": { + "tags": [] + }, "source": [ "# Line plots\n", "\n", @@ -642,6 +658,18 @@ "This example plots a sine wave, cosine wave, and ricker wavelet and demonstrates how **Graphic Features** can be modified by slicing!" ] }, + { + "cell_type": "code", + "execution_count": 15, + "id": "b69b8edd-f87f-406d-af56-e851d4fc6e77", + "metadata": {}, + "outputs": [], + "source": [ + "from fastplotlib import Plot\n", + "from ipywidgets import VBox, HBox, IntSlider\n", + "import numpy as np" + ] + }, { "cell_type": "markdown", "id": "a6fee1c2-4a24-4325-bca2-26e5a4bf6338", @@ -652,7 +680,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 16, "id": "8e8280da-b421-43a5-a1a6-2a196a408e9a", "metadata": {}, "outputs": [], @@ -670,7 +698,7 @@ "# sinc function\n", "a = 0.5\n", "ys = np.sinc(xs) * 3 + 8\n", - "sinc = np.dstack([xs, ys])[0]\n" + "sinc = np.dstack([xs, ys])[0]" ] }, { @@ -683,14 +711,14 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 17, "id": "93a5d1e6-d019-4dd0-a0d1-25d1704ab7a7", "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "d81c444c49124d7d9bbb88a9c64690fa", + "model_id": "380434748ed44486979846c314606408", "version_major": 2, "version_minor": 0 }, @@ -704,7 +732,7 @@ { "data": { "text/html": [ - "
initial snapshot
" + "
initial snapshot
" ], "text/plain": [ "" @@ -716,7 +744,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "85e6e88bd68b4255b190bf7563ad77e4", + "model_id": "f1cab77b2f60497ab52fbf19764146ad", "version_major": 2, "version_minor": 0 }, @@ -724,7 +752,7 @@ "JupyterWgpuCanvas()" ] }, - "execution_count": 14, + "execution_count": 17, "metadata": {}, "output_type": "execute_result" } @@ -734,7 +762,7 @@ "plot_l = Plot()\n", "\n", "# plot sine wave, use a single color\n", - "sine_graphic = plot_l.add_line(data=sine, size=1.5, colors=\"magenta\")\n", + "sine_graphic = plot_l.add_line(data=sine, size=5, colors=\"magenta\")\n", "\n", "# you can also use colormaps for lines!\n", "cosine_graphic = plot_l.add_line(data=cosine, size=12, cmap=\"autumn\")\n", @@ -756,17 +784,21 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 18, "id": "cb0d13ed-ef07-46ff-b19e-eeca4c831037", "metadata": {}, "outputs": [], "source": [ - "# fancy indexing of colors\n", + "# indexing of colors\n", "cosine_graphic.colors[:15] = \"magenta\"\n", "cosine_graphic.colors[90:] = \"red\"\n", "cosine_graphic.colors[60] = \"w\"\n", "\n", - "# more complex indexing, set the blue value from an array\n", + "# indexing to assign colormaps to entire lines or segments\n", + "sinc_graphic.cmap[10:50] = \"gray\"\n", + "sine_graphic.cmap = \"seismic\"\n", + "\n", + "# more complex indexing, set the blue value directly from an array\n", "cosine_graphic.colors[65:90, 0] = np.linspace(0, 1, 90-65)" ] }, @@ -775,12 +807,12 @@ "id": "c9689887-cdf3-4a4d-948f-7efdb09bde4e", "metadata": {}, "source": [ - "## You can capture changes to a graphic features as events" + "## You can capture changes to a graphic feature as events" ] }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 19, "id": "cfa001f6-c640-4f91-beb0-c19b030e503f", "metadata": {}, "outputs": [], @@ -794,7 +826,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 20, "id": "bb8a0f95-0063-4cd4-a117-e3d62c6e120d", "metadata": {}, "outputs": [ @@ -802,9 +834,9 @@ "name": "stdout", "output_type": "stream", "text": [ - "FeatureEvent @ 0x7fa1f3a9ac50\n", + "FeatureEvent @ 0x7f7429bca830\n", "type: color-changed\n", - "pick_info: {'index': range(15, 50, 3), 'world_object': , 'new_data': array([[0., 1., 1., 1.],\n", + "pick_info: {'index': range(15, 50, 3), 'world_object': , 'new_data': array([[0., 1., 1., 1.],\n", " [0., 1., 1., 1.],\n", " [0., 1., 1., 1.],\n", " [0., 1., 1., 1.],\n", @@ -836,7 +868,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 21, "id": "d1a4314b-5723-43c7-94a0-b4cbb0e44d60", "metadata": {}, "outputs": [], @@ -847,7 +879,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 22, "id": "682db47b-8c7a-4934-9be4-2067e9fb12d5", "metadata": {}, "outputs": [], @@ -865,7 +897,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 23, "id": "fcba75b7-9a1e-4aae-9dec-715f7f7456c3", "metadata": {}, "outputs": [], @@ -875,7 +907,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 24, "id": "763b9943-53a4-4e2a-b47a-4e9e5c9d7be3", "metadata": {}, "outputs": [], @@ -893,7 +925,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 25, "id": "64a20a16-75a5-4772-a849-630ade9be4ff", "metadata": {}, "outputs": [], @@ -903,7 +935,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 26, "id": "fb093046-c94c-4085-86b4-8cd85cb638ff", "metadata": {}, "outputs": [], @@ -913,7 +945,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 27, "id": "f05981c3-c768-4631-ae62-6a8407b20c4c", "metadata": {}, "outputs": [], @@ -931,14 +963,14 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 28, "id": "9c51229f-13a2-4653-bff3-15d43ddbca7b", "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "ee8725a8c6944a799eac247a98737688", + "model_id": "8462601c909049dda4e8fdcbb526c6f6", "version_major": 2, "version_minor": 0 }, @@ -960,7 +992,7 @@ { "data": { "text/html": [ - "
initial snapshot
" + "
initial snapshot
" ], "text/plain": [ "" @@ -972,7 +1004,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "d1595cc3616e4018902c09e65cd99f40", + "model_id": "8ae62d829add48c1b5f26f9633b5b0ed", "version_major": 2, "version_minor": 0 }, @@ -980,7 +1012,7 @@ "JupyterWgpuCanvas()" ] }, - "execution_count": 25, + "execution_count": 28, "metadata": {}, "output_type": "execute_result" } @@ -1019,14 +1051,26 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 29, + "id": "2ecb2385-8fa4-4239-881c-b754c24aed9f", + "metadata": {}, + "outputs": [], + "source": [ + "from fastplotlib import Plot\n", + "from ipywidgets import VBox, HBox, IntSlider\n", + "import numpy as np" + ] + }, + { + "cell_type": "code", + "execution_count": 30, "id": "39252df5-9ae5-4132-b97b-2785c5fa92ea", "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "9a091850b30e49b7b04153d6604176aa", + "model_id": "50c0b259d1f04e239dfbf733463fac3e", "version_major": 2, "version_minor": 0 }, @@ -1040,7 +1084,7 @@ { "data": { "text/html": [ - "
initial snapshot
" + "
initial snapshot
" ], "text/plain": [ "" @@ -1052,7 +1096,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "cc57fd6c0807411bbde07b968fe7deef", + "model_id": "c138433d6890443faeab644cc5521b0d", "version_major": 2, "version_minor": 0 }, @@ -1060,7 +1104,7 @@ "JupyterWgpuCanvas()" ] }, - "execution_count": 26, + "execution_count": 30, "metadata": {}, "output_type": "execute_result" } @@ -1111,7 +1155,7 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 31, "id": "8fa46ec0-8680-44f5-894c-559de3145932", "metadata": {}, "outputs": [], @@ -1122,7 +1166,7 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 32, "id": "e4dc71e4-5144-436f-a464-f2a29eee8f0b", "metadata": {}, "outputs": [], @@ -1133,7 +1177,7 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": 33, "id": "5b637a29-cd5e-4011-ab81-3f91490d9ecd", "metadata": {}, "outputs": [], @@ -1144,7 +1188,7 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 34, "id": "a4084fce-78a2-48b3-9a0d-7b57c165c3c1", "metadata": {}, "outputs": [], @@ -1155,7 +1199,7 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 35, "id": "f486083e-7c58-4255-ae1a-3fe5d9bfaeed", "metadata": {}, "outputs": [], @@ -1176,19 +1220,19 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": 36, "id": "f404a5ea-633b-43f5-87d1-237017bbca2a", "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "62aa7a1eb8794301912c4f2da74abb25", + "model_id": "38cd7d8eacf3493c9bddd01ee3ec40f4", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "VBox(children=(HBox(children=(JupyterWgpuCanvas(frame_feedback={'index': 2402, 'timestamp': 1671971185.3935852…" + "VBox(children=(HBox(children=(JupyterWgpuCanvas(frame_feedback={'index': 123, 'timestamp': 1671994226.4736164,…" ] }, "metadata": {}, diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index a1a2633b9..3cf358451 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -1,63 +1,33 @@ from typing import * -import pygfx +from pygfx import WorldObject +from pygfx.linalg import Vector3 -from ..utils import get_colors -from .features import GraphicFeature, DataFeature, ColorFeature, PresentFeature +from .features import GraphicFeature, PresentFeature -class Graphic: +class BaseGraphic: + def __init_subclass__(cls, **kwargs): + """set the type of the graphic in lower case like "image", "line_collection", etc.""" + cls.type = cls.__name__.lower().replace("graphic", "").replace("collection", "_collection") + super().__init_subclass__(**kwargs) + + +class Graphic(BaseGraphic): def __init__( self, - data, - colors: Any = False, - n_colors: int = None, - cmap: str = None, - alpha: float = 1.0, name: str = None ): """ Parameters ---------- - data: array-like - data to show in the graphic, must be float32. - Automatically converted to float32 for numpy arrays. - Tensorflow Tensors also work but this is not fully - tested and might not be supported in the future. - - colors: Any - if ``False``, no color generation is performed, cmap is also ignored. - - n_colors - - cmap: str - name of colormap to use - - alpha: float, optional - alpha value for the colors - name: str, optional name this graphic, makes it indexable within plots """ - # self.data = data.astype(np.float32) - self.data = DataFeature(parent=self, data=data, graphic_name=self.__class__.__name__) - self.colors = None self.name = name - - if n_colors is None: - n_colors = self.data.feature_data.shape[0] - - if cmap is not None and colors is not False: - colors = get_colors(n_colors=n_colors, cmap=cmap, alpha=alpha) - - if colors is not False: - self.colors = ColorFeature(parent=self, colors=colors, n_colors=n_colors, alpha=alpha) - - # different from visible, toggles the Graphic presence in the Scene - # useful for bbox calculations to ignore these Graphics self.present = PresentFeature(parent=self) valid_features = ["visible"] @@ -69,9 +39,14 @@ def __init__( self._valid_features = tuple(valid_features) @property - def world_object(self) -> pygfx.WorldObject: + def world_object(self) -> WorldObject: return self._world_object + @property + def position(self) -> Vector3: + """The position of the graphic""" + return self.world_object.position + @property def interact_features(self) -> Tuple[str]: """The features for this ``Graphic`` that support interaction.""" @@ -87,7 +62,7 @@ def visible(self, v): self.world_object.visible = v @property - def children(self) -> pygfx.WorldObject: + def children(self) -> WorldObject: return self.world_object.children def __setattr__(self, key, value): diff --git a/fastplotlib/graphics/features/__init__.py b/fastplotlib/graphics/features/__init__.py index 2c489c94f..1fcb71246 100644 --- a/fastplotlib/graphics/features/__init__.py +++ b/fastplotlib/graphics/features/__init__.py @@ -1,4 +1,4 @@ -from ._colors import ColorFeature -from ._data import DataFeature +from ._colors import ColorFeature, CmapFeature, ImageCmapFeature +from ._data import PointsDataFeature, ImageDataFeature from ._present import PresentFeature from ._base import GraphicFeature diff --git a/fastplotlib/graphics/features/_base.py b/fastplotlib/graphics/features/_base.py index 9292f4944..519bf40d0 100644 --- a/fastplotlib/graphics/features/_base.py +++ b/fastplotlib/graphics/features/_base.py @@ -90,6 +90,22 @@ def _call_event_handlers(self, event_data: FeatureEvent): def cleanup_slice(key: Union[int, slice], upper_bound) -> Union[slice, int]: + """ + + If the key in an `int`, it just returns it. Otherwise, + it parses it and removes the `None` vals and replaces + them with corresponding values that can be used to + create a `range`, get `len` etc. + + Parameters + ---------- + key + upper_bound + + Returns + ------- + + """ if isinstance(key, int): return key @@ -157,7 +173,7 @@ def _upper_bound(self) -> int: return self.feature_data.shape[0] def _update_range_indices(self, key): - """Currently used by colors and data""" + """Currently used by colors and positions data""" key = cleanup_slice(key, self._upper_bound) if isinstance(key, int): @@ -178,5 +194,3 @@ def _update_range_indices(self, key): self._buffer.update_range(ix, size=1) else: raise TypeError("must pass int or slice to update range") - - diff --git a/fastplotlib/graphics/features/_colors.py b/fastplotlib/graphics/features/_colors.py index f45f99040..afb0d85a8 100644 --- a/fastplotlib/graphics/features/_colors.py +++ b/fastplotlib/graphics/features/_colors.py @@ -1,6 +1,7 @@ import numpy as np -from ._base import GraphicFeatureIndexable, cleanup_slice, FeatureEvent +from ._base import GraphicFeature, GraphicFeatureIndexable, cleanup_slice, FeatureEvent +from ...utils import get_colors, get_cmap_texture from pygfx import Color @@ -15,7 +16,7 @@ def __getitem__(self, item): def __repr__(self): return repr(self._buffer.data) - def __init__(self, parent, colors, n_colors, alpha: float = 1.0): + def __init__(self, parent, colors, n_colors: int, alpha: float = 1.0): """ ColorFeature @@ -27,7 +28,12 @@ def __init__(self, parent, colors, n_colors, alpha: float = 1.0): specify colors as a single human readable string, RGBA array, or an iterable of strings or RGBA arrays - n_colors: number of colors to hold, if passing in a single str or single RGBA array + n_colors: int + number of colors to hold, if passing in a single str or single RGBA array + + alpha: float + alpha value for the colors + """ # if provided as a numpy array of str if isinstance(colors, np.ndarray): @@ -185,3 +191,55 @@ def _feature_changed(self, key, new_data): event_data = FeatureEvent(type="color-changed", pick_info=pick_info) self._call_event_handlers(event_data) + + +class CmapFeature(ColorFeature): + """ + Indexable colormap feature, mostly wraps colors and just provides a way to set colormaps. + """ + def __init__(self, parent, colors): + super(ColorFeature, self).__init__(parent, colors) + + def __setitem__(self, key, value): + key = cleanup_slice(key, self._upper_bound) + if not isinstance(key, slice): + raise TypeError("Cannot set cmap on single indices, must pass a slice object or " + "set it on the entire data.") + + n_colors = len(range(key.start, key.stop, key.step)) + + colors = get_colors(n_colors, cmap=value) + super(CmapFeature, self).__setitem__(key, colors) + + +class ImageCmapFeature(GraphicFeature): + """ + Colormap for ImageGraphic + """ + def __init__(self, parent, cmap: str): + cmap_texture_view = get_cmap_texture(cmap) + super(ImageCmapFeature, self).__init__(parent, cmap_texture_view) + self.name = cmap + + def _set(self, cmap_name: str): + self._parent.world_object.material.map.texture.data[:] = get_colors(256, cmap_name) + self._parent.world_object.material.map.texture.update_range((0, 0, 0), size=(256, 1, 1)) + self.name = cmap_name + + self._feature_changed(key=None, new_data=self.name) + + def __repr__(self): + return repr(self.name) + + def _feature_changed(self, key, new_data): + # this is a non-indexable feature so key=None + + pick_info = { + "index": None, + "world_object": self._parent.world_object, + "new_data": new_data + } + + event_data = FeatureEvent(type="cmap-changed", pick_info=pick_info) + + self._call_event_handlers(event_data) diff --git a/fastplotlib/graphics/features/_data.py b/fastplotlib/graphics/features/_data.py index 6e1feac2a..1839bd9a1 100644 --- a/fastplotlib/graphics/features/_data.py +++ b/fastplotlib/graphics/features/_data.py @@ -1,32 +1,30 @@ -from ._base import GraphicFeatureIndexable, cleanup_slice, FeatureEvent -from pygfx import Buffer from typing import * -from ...utils import fix_data, to_float32 + +import numpy as np +from pygfx import Buffer, Texture + +from ._base import GraphicFeatureIndexable, cleanup_slice, FeatureEvent -class DataFeature(GraphicFeatureIndexable): +def to_float32(array): + if isinstance(array, np.ndarray): + return array.astype(np.float32, copy=False) + + return array + + +class PointsDataFeature(GraphicFeatureIndexable): """ - Access to the buffer data being shown in the graphic. - Supports fancy indexing if the data array also does. + Access to the vertex buffer data shown in the graphic. + Supports fancy indexing if the data array also supports it. """ - # the correct data buffer is search for in this order - data_buffer_names = ["grid", "positions"] - - def __init__(self, parent, data: Any, graphic_name): - data = fix_data(data, graphic_name=graphic_name) - self.graphic_name = graphic_name - super(DataFeature, self).__init__(parent, data) + def __init__(self, parent, data: Any): + data = self._fix_data(data, parent) + super(PointsDataFeature, self).__init__(parent, data) @property def _buffer(self) -> Buffer: - buffer = getattr(self._parent.world_object.geometry, self._buffer_name) - return buffer - - @property - def _buffer_name(self) -> str: - for buffer_name in self.data_buffer_names: - if hasattr(self._parent.world_object.geometry, buffer_name): - return buffer_name + return self._parent.world_object.geometry.positions def __repr__(self): return repr(self._buffer.data) @@ -34,31 +32,103 @@ def __repr__(self): def __getitem__(self, item): return self._buffer.data[item] + def _fix_data(self, data, parent): + graphic_type = parent.__class__.__name__ + + if data.ndim == 1: + # for scatter if we receive just 3 points in a 1d array, treat it as just a single datapoint + # this is different from fix_data for LineGraphic since there we assume that a 1d array + # is just y-values + if graphic_type == "ScatterGraphic": + data = np.array([data]) + elif graphic_type == "LineGraphic": + data = np.dstack([np.arange(data.size), data])[0].astype(np.float32) + + if data.shape[1] != 3: + if data.shape[1] != 2: + raise ValueError(f"Must pass 1D, 2D or 3D data to {graphic_type}") + + # zeros for z + zs = np.zeros(data.shape[0], dtype=np.float32) + + data = np.dstack([data[:, 0], data[:, 1], zs])[0] + + return data + def __setitem__(self, key, value): + # put data into right shape if they're only indexing datapoints if isinstance(key, (slice, int)): - # data must be provided in the right shape - value = fix_data(value, graphic_name=self.graphic_name) - else: - # otherwise just make sure float32 - value = to_float32(value) + value = self._fix_data(value, self._parent) + # otherwise assume that they have the right shape + # numpy will throw errors if it can't broadcast + self._buffer.data[key] = value self._update_range(key) + # avoid creating dicts constantly if there are no events to handle + if len(self._event_handlers) > 0: + self._feature_changed(key, value) def _update_range(self, key): - if self._buffer_name == "grid": - self._update_range_grid(key) - self._feature_changed(key=None, new_data=None) - elif self._buffer_name == "positions": - self._update_range_indices(key) - self._feature_changed(key=key, new_data=None) + self._update_range_indices(key) + + def _feature_changed(self, key, new_data): + if key is not None: + key = cleanup_slice(key, self._upper_bound) + if isinstance(key, int): + indices = [key] + elif isinstance(key, slice): + indices = range(key.start, key.stop, key.step) + elif key is None: + indices = None + + pick_info = { + "index": indices, + "world_object": self._parent.world_object, + "new_data": new_data + } + + event_data = FeatureEvent(type="data-changed", pick_info=pick_info) + + self._call_event_handlers(event_data) - def _update_range_grid(self, key): - # image data - self._buffer.update_range((0, 0, 0), self._buffer.size) + +class ImageDataFeature(GraphicFeatureIndexable): + """ + Access to the TextureView buffer shown in an ImageGraphic. + """ + + def __init__(self, parent, data: Any): + if data.ndim != 2: + raise ValueError("`data.ndim !=2`, you must pass only a 2D array to an Image graphic") + + data = to_float32(data) + super(ImageDataFeature, self).__init__(parent, data) + + @property + def _buffer(self) -> Texture: + return self._parent.world_object.geometry.grid.texture + + def __repr__(self): + return repr(self._buffer.data) + + def __getitem__(self, item): + return self._buffer.data[item] + + def __setitem__(self, key, value): + # make sure float32 + value = to_float32(value) + + self._buffer.data[key] = value + self._update_range(key) + + # avoid creating dicts constantly if there are no events to handle + if len(self._event_handlers) > 0: + self._feature_changed(key, value) + + def _update_range(self, key): + self._buffer.update_range((0, 0, 0), size=self._buffer.size) def _feature_changed(self, key, new_data): - # for now if key=None that means all data changed, i.e. ImageGraphic - # also for now new data isn't stored for DataFeature if key is not None: key = cleanup_slice(key, self._upper_bound) if isinstance(key, int): diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index 77c531c8a..48459c63e 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -1,10 +1,10 @@ from typing import * -import numpy as np import pygfx from ._base import Graphic -from ..utils import quick_min_max, get_cmap_texture +from .features import ImageCmapFeature, ImageDataFeature +from ..utils import quick_min_max class ImageGraphic(Graphic): @@ -14,6 +14,7 @@ def __init__( vmin: int = None, vmax: int = None, cmap: str = 'plasma', + filter: str = "nearest", *args, **kwargs ): @@ -32,10 +33,15 @@ def __init__( vmax: int, optional maximum value for color scaling, calculated from data if not provided - cmap: str, optional + cmap: str, optional, default "nearest" colormap to use to display the image data, default is ``"plasma"`` + + filter: str, optional, default "nearest" + interpolation filter, one of "nearest" or "linear" + args: additional arguments passed to Graphic + kwargs: additional keyword arguments passed to Graphic @@ -59,23 +65,41 @@ def __init__( plot.show() """ - if data.ndim != 2: - raise ValueError("`data.ndim !=2`, you must pass only a 2D array to `data`") - super().__init__(data, cmap=cmap, *args, **kwargs) + super().__init__(*args, **kwargs) + + self.data = ImageDataFeature(self, data) if (vmin is None) or (vmax is None): vmin, vmax = quick_min_max(data) + self.cmap = ImageCmapFeature(self, cmap) + + texture_view = pygfx.Texture(self.data.feature_data, dim=2).get_view(filter=filter) + self._world_object: pygfx.Image = pygfx.Image( - pygfx.Geometry(grid=pygfx.Texture(self.data.feature_data, dim=2)), - pygfx.ImageBasicMaterial(clim=(vmin, vmax), map=get_cmap_texture(cmap)) + pygfx.Geometry(grid=texture_view), + pygfx.ImageBasicMaterial(clim=(vmin, vmax), map=self.cmap.feature_data) ) @property - def clim(self) -> Tuple[float, float]: - return self.world_object.material.clim + def vmin(self) -> float: + return self.world_object.material.clim[0] + + @vmin.setter + def vmin(self, value: float): + self.world_object.material.clim = ( + value, + self.world_object.material.clim[1] + ) - @clim.setter - def clim(self, levels: Tuple[float, float]): - self.world_object.material.clim = levels + @property + def vmax(self) -> float: + return self.world_object.material.clim[1] + + @vmax.setter + def vmax(self, value: float): + self.world_object.material.clim = ( + self.world_object.material.clim[0], + value + ) diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index edf99e43c..e53eb9203 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -3,16 +3,19 @@ import pygfx from ._base import Graphic +from .features import PointsDataFeature, ColorFeature, CmapFeature +from ..utils import get_colors class LineGraphic(Graphic): def __init__( self, data: Any, - z_position: float = 0.0, size: float = 2.0, colors: Union[str, np.ndarray, Iterable] = "w", + alpha: float = 1.0, cmap: str = None, + z_position: float = 0.0, *args, **kwargs ): @@ -24,34 +27,46 @@ def __init__( data: array-like Line data to plot, 2D must be of shape [n_points, 2], 3D must be of shape [n_points, 3] - z_position: float, optional - z-axis position for placing the graphic - - size: float, optional + size: float, optional, default 2.0 thickness of the line - colors: str, array, or iterable + colors: str, array, or iterable, default "w" specify colors as a single human readable string, a single RGBA array, or an iterable of strings or RGBA arrays cmap: str, optional - apply a colormap to the line instead of assigning colors manually + apply a colormap to the line instead of assigning colors manually, this + overrides any argument passed to "colors" + + alpha: float, optional, default 1.0 + alpha value for the colors + + z_position: float, optional + z-axis position for placing the graphic args passed to Graphic + kwargs passed to Graphic + """ - super(LineGraphic, self).__init__(data, colors=colors, cmap=cmap, *args, **kwargs) + self.data = PointsDataFeature(self, data) + + if cmap is not None: + colors = get_colors(n_colors=self.data.feature_data.shape[0], cmap=cmap, alpha=alpha) + + self.colors = ColorFeature(self, colors, n_colors=self.data.feature_data.shape[0], alpha=alpha) + self.cmap = CmapFeature(self, self.colors.feature_data) + + super(LineGraphic, self).__init__(*args, **kwargs) if size < 1.1: material = pygfx.LineThinMaterial else: material = pygfx.LineMaterial - # self.data = np.ascontiguousarray(self.data) - self._world_object: pygfx.Line = pygfx.Line( # self.data.feature_data because data is a Buffer geometry=pygfx.Geometry(positions=self.data.feature_data, colors=self.colors.feature_data), diff --git a/fastplotlib/graphics/linecollection.py b/fastplotlib/graphics/linecollection.py index ec4b1e4dd..cfdf5389f 100644 --- a/fastplotlib/graphics/linecollection.py +++ b/fastplotlib/graphics/linecollection.py @@ -1,11 +1,12 @@ import numpy as np import pygfx from typing import Union +from ._base import BaseGraphic from .line import LineGraphic from typing import * -class LineCollection(): +class LineCollection(BaseGraphic): def __init__(self, data: List[np.ndarray], z_position: Union[List[float], float] = None, size: Union[float, List[float]] = 2.0, colors: Union[List[np.ndarray], np.ndarray] = None, cmap: Union[List[str], str] = None, *args, **kwargs): diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index 0ea5a8831..a1083d132 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -4,11 +4,61 @@ import pygfx from ._base import Graphic +from .features import PointsDataFeature, ColorFeature, CmapFeature +from ..utils import get_colors class ScatterGraphic(Graphic): - def __init__(self, data: np.ndarray, z_position: float = 0.0, sizes: Union[int, np.ndarray, list] = 1, colors: np.ndarray = "w", cmap: str = None, *args, **kwargs): - super(ScatterGraphic, self).__init__(data, colors=colors, cmap=cmap, *args, **kwargs) + def __init__( + self, + data: np.ndarray, + sizes: Union[int, np.ndarray, list] = 1, + colors: np.ndarray = "w", + alpha: float = 1.0, + cmap: str = None, + z_position: float = 0.0, + *args, + **kwargs + ): + """ + Create a Scatter Graphic, 2d or 3d + + Parameters + ---------- + data: array-like + Scatter data to plot, 2D must be of shape [n_points, 2], 3D must be of shape [n_points, 3] + + sizes: float or iterable of float, optional, default 1.0 + size of the scatter points + + colors: str, array, or iterable, default "w" + specify colors as a single human readable string, a single RGBA array, + or an iterable of strings or RGBA arrays + + cmap: str, optional + apply a colormap to the scatter instead of assigning colors manually, this + overrides any argument passed to "colors" + + alpha: float, optional, default 1.0 + alpha value for the colors + + z_position: float, optional + z-axis position for placing the graphic + + args + passed to Graphic + + kwargs + passed to Graphic + + """ + self.data = PointsDataFeature(self, data) + + if cmap is not None: + colors = get_colors(n_colors=self.data.feature_data.shape[0], cmap=cmap, alpha=alpha) + + self.colors = ColorFeature(self, colors, n_colors=self.data.feature_data.shape[0], alpha=alpha) + self.cmap = CmapFeature(self, self.colors.feature_data) if isinstance(sizes, int): sizes = np.full(self.data.feature_data.shape[0], sizes, dtype=np.float32) @@ -20,6 +70,8 @@ def __init__(self, data: np.ndarray, z_position: float = 0.0, sizes: Union[int, if len(sizes) != self.data.feature_data.shape[0]: raise ValueError("list of `sizes` must have the same length as the number of datapoints") + super(ScatterGraphic, self).__init__(*args, **kwargs) + self._world_object: pygfx.Points = pygfx.Points( pygfx.Geometry(positions=self.data.feature_data, sizes=sizes, colors=self.colors.feature_data), material=pygfx.PointsMaterial(vertex_colors=True, vertex_sizes=True) diff --git a/fastplotlib/graphics/text.py b/fastplotlib/graphics/text.py index ed47bd270..0096e102c 100644 --- a/fastplotlib/graphics/text.py +++ b/fastplotlib/graphics/text.py @@ -2,8 +2,10 @@ import pygfx import numpy as np +from ._base import BaseGraphic -class TextGraphic: + +class TextGraphic(BaseGraphic): def __init__( self, text: str, diff --git a/fastplotlib/layouts/_subplot.py b/fastplotlib/layouts/_subplot.py index c8e8d2b23..7a0e923de 100644 --- a/fastplotlib/layouts/_subplot.py +++ b/fastplotlib/layouts/_subplot.py @@ -6,7 +6,7 @@ from warnings import warn from pygfx import Scene, OrthographicCamera, PanZoomController, OrbitOrthoController, \ - AxesHelper, GridHelper, WgpuRenderer, Background, BackgroundMaterial + AxesHelper, GridHelper, WgpuRenderer from wgpu.gui.auto import WgpuCanvas from ._base import PlotArea @@ -82,8 +82,8 @@ def __init__( pfunc.__signature__ = signature(cls) pfunc.__doc__ = cls.__init__.__doc__ - graphic_cls_name = graphic_cls_name.lower().replace("graphic", "").replace("collection", "_collection") - setattr(self, f"add_{graphic_cls_name}", pfunc) + # cls.type is defined in Graphic.__init_subclass__ + setattr(self, f"add_{cls.type}", pfunc) self._title_graphic: TextGraphic = None if self.name is not None: @@ -130,13 +130,13 @@ def get_rect(self): x_pos = ((width_canvas / self.ncols) + ((col_ix - 1) * (width_canvas / self.ncols))) + self.spacing y_pos = ((height_canvas / self.nrows) + ((row_ix - 1) * (height_canvas / self.nrows))) + self.spacing width_subplot = (width_canvas / self.ncols) - self.spacing - height_suplot = (height_canvas / self.nrows) - self.spacing + height_subplot = (height_canvas / self.nrows) - self.spacing rect = np.array([ x_pos, y_pos, width_subplot, - height_suplot + height_subplot ]) for dv in self.docked_viewports.values(): @@ -221,6 +221,7 @@ def remove_animation(self, func): self._animate_funcs_post.remove(func) def add_graphic(self, graphic, center: bool = True): + graphic.world_object.position.z = len(self._graphics) super(Subplot, self).add_graphic(graphic, center) if isinstance(graphic, graphics.HeatmapGraphic): diff --git a/fastplotlib/utils/functions.py b/fastplotlib/utils/functions.py index 4df784b6e..698d20113 100644 --- a/fastplotlib/utils/functions.py +++ b/fastplotlib/utils/functions.py @@ -88,36 +88,3 @@ def quick_min_max(data: np.ndarray) -> Tuple[float, float]: data = data[tuple(sl)] return float(np.nanmin(data)), float(np.nanmax(data)) - - -def to_float32(array): - if isinstance(array, np.ndarray): - return array.astype(np.float32, copy=False) - - return array - - -def fix_data(array, graphic_name: str) -> np.ndarray: - """1d or 2d to 3d, cleanup data passed from user before instantiating any Graphic class""" - if graphic_name == "ImageGraphic": - return to_float32(array) - - if array.ndim == 1: - # for scatter if we receive just 3 points in a 1d array, treat it as just a single datapoint - # this is different from fix_data for LineGraphic since there we assume that a 1d array - # is just y-values - if graphic_name == "ScatterGraphic": - array = np.array([array]) - elif graphic_name == "LineGraphic": - array = np.dstack([np.arange(array.size), array])[0].astype(np.float32) - - if array.shape[1] != 3: - if array.shape[1] != 2: - raise ValueError(f"Must pass 1D, 2D or 3D data to {graphic_name}") - - # zeros for z - zs = np.zeros(array.shape[0], dtype=np.float32) - - array = np.dstack([array[:, 0], array[:, 1], zs])[0] - - return array diff --git a/fastplotlib/widgets/image.py b/fastplotlib/widgets/image.py index 5fba44d56..f8dc6c73f 100644 --- a/fastplotlib/widgets/image.py +++ b/fastplotlib/widgets/image.py @@ -472,8 +472,8 @@ def __init__( max=minmax[1] + data_range_30p, step=data_range / 150, description=f"min-max", - readout = True, - readout_format = '.3f', + readout=True, + readout_format='.3f', ) minmax_slider.observe( @@ -779,7 +779,9 @@ def _vmin_vmax_slider_changed( data_ix: int, change: dict ): - self.image_graphics[data_ix].clim = change["new"] + vmin, vmax = change["new"] + self.image_graphics[data_ix].vmin = vmin + self.image_graphics[data_ix].vmax = vmax def _set_slider_layout(self, *args): w, h = self.plot.renderer.logical_size