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 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy