diff --git a/examples/gridplot.ipynb b/examples/gridplot.ipynb index d93295bbc..512c362d7 100644 --- a/examples/gridplot.ipynb +++ b/examples/gridplot.ipynb @@ -28,7 +28,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "5d26f20d062b4d3eba78c6fb1a70d228", + "model_id": "77d5643b30ac468f8d26322edab10f2d", "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": "745191726e1c44cbb338cf087d79728b", + "model_id": "2ff6060dc64545d28c480b0ede36c6d9", "version_major": 2, "version_minor": 0 }, @@ -109,7 +109,7 @@ "def set_random_frame():\n", " for ig in image_graphics:\n", " new_data = np.random.rand(512, 512)\n", - " ig.update_data(data=new_data)\n", + " ig.data = new_data\n", "\n", "# add the animation\n", "grid_plot.add_animations(set_random_frame)\n", @@ -133,10 +133,10 @@ { "data": { "text/plain": [ - "subplot0: Subplot @ 0x7fd08bbeb730\n", + "subplot0: Subplot @ 0x7f3c6012a8c0\n", " parent: None\n", " Graphics:\n", - "\t'image' fastplotlib.ImageGraphic @ 0x7fd08bbebfa0" + "\t'image' fastplotlib.ImageGraphic @ 0x7f3c6012a8f0" ] }, "execution_count": 3, @@ -158,10 +158,10 @@ { "data": { "text/plain": [ - "subplot0: Subplot @ 0x7fd08bbeb730\n", + "subplot0: Subplot @ 0x7f3c6012a8c0\n", " parent: None\n", " Graphics:\n", - "\t'image' fastplotlib.ImageGraphic @ 0x7fd08bbebfa0" + "\t'image' fastplotlib.ImageGraphic @ 0x7f3c6012a8f0" ] }, "execution_count": 4, @@ -192,7 +192,7 @@ { "data": { "text/plain": [ - "'image' fastplotlib.ImageGraphic @ 0x7fd08bbebfa0" + "'image' fastplotlib.ImageGraphic @ 0x7f3c6012a8f0" ] }, "execution_count": 5, @@ -236,7 +236,7 @@ { "cell_type": "code", "execution_count": null, - "id": "2c69b9dc-fb21-4515-a145-4ba0c04cacb1", + "id": "a025b76c-77f8-4aeb-ac33-5bb6d0bb5a9a", "metadata": {}, "outputs": [], "source": [] diff --git a/examples/gridplot_simple.ipynb b/examples/gridplot_simple.ipynb index cf99bac7b..ee8a88983 100644 --- a/examples/gridplot_simple.ipynb +++ b/examples/gridplot_simple.ipynb @@ -28,7 +28,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "f67bb4e00c0442d6b6fbd2eda11e5f9c", + "model_id": "4a46061e2aca46aeb6dd21faef1c3ba3", "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": "015b67891ce94d569c3f42c67f1c4e16", + "model_id": "5ced1c73cc114f25875aebf367282a5c", "version_major": 2, "version_minor": 0 }, @@ -91,7 +91,7 @@ "def update_data():\n", " for ig in image_graphics:\n", " new_data = np.random.rand(512, 512)\n", - " ig.update_data(data=new_data)\n", + " ig.data = new_data\n", "\n", "# add the animation function\n", "grid_plot.add_animations(update_data)\n", @@ -117,10 +117,10 @@ { "data": { "text/plain": [ - "unnamed: Subplot @ 0x7efdd43e78e0\n", + "unnamed: Subplot @ 0x7fd48d3a96f0\n", " parent: None\n", " Graphics:\n", - "\tfastplotlib.ImageGraphic @ 0x7efdc790beb0" + "\tfastplotlib.ImageGraphic @ 0x7fd486b9fd60" ] }, "execution_count": 3, @@ -151,7 +151,7 @@ { "data": { "text/plain": [ - "[fastplotlib.ImageGraphic @ 0x7efdc7925120]" + "[fastplotlib.ImageGraphic @ 0x7fd486b85f00]" ] }, "execution_count": 4, @@ -209,10 +209,10 @@ { "data": { "text/plain": [ - "top-right-plot: Subplot @ 0x7efdd4222d70\n", + "top-right-plot: Subplot @ 0x7fd486b00a90\n", " parent: None\n", " Graphics:\n", - "\tfastplotlib.ImageGraphic @ 0x7efdc7970070" + "\tfastplotlib.ImageGraphic @ 0x7fd486bd5fc0" ] }, "execution_count": 7, diff --git a/examples/lineplot.ipynb b/examples/lineplot.ipynb index 630fac3cd..7561efe88 100644 --- a/examples/lineplot.ipynb +++ b/examples/lineplot.ipynb @@ -30,7 +30,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "f1faeefaf48a443cbc8a3b37d0c0d076", + "model_id": "68a29ed7dad343ee9191b9887f3ed47b", "version_major": 2, "version_minor": 0 }, @@ -52,7 +52,7 @@ { "data": { "text/html": [ - "
initial snapshot
" + "
initial snapshot
" ], "text/plain": [ "" @@ -64,7 +64,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "eb81231750b04c149dc8bcd7a12d50c0", + "model_id": "3a918fe2ec1a403294f808032fb4133c", "version_major": 2, "version_minor": 0 }, @@ -127,7 +127,7 @@ " if i == 2:\n", " subplot.camera.scale.y = -1\n", " \n", - " marker = subplot.add_scatter(data=spiral[0], size=10)\n", + " marker = subplot.add_scatter(data=spiral[0], sizes=10)\n", " markers.append(marker)\n", " \n", "marker_index = 0\n", @@ -142,14 +142,11 @@ " if marker_index == spiral.shape[0]:\n", " marker_index = 0\n", " \n", - " new_markers = list()\n", + " # new_markers = list()\n", " for subplot, marker in zip(grid_plot, markers):\n", - " subplot.remove_graphic(marker)\n", - " new_marker = subplot.add_scatter(data=spiral[marker_index], size=15)\n", - " new_markers.append(new_marker)\n", + " pass\n", + " marker.data = spiral[marker_index]\n", " \n", - " markers = new_markers\n", - "\n", "# add `move_marker` to the animations\n", "grid_plot.add_animations(move_marker)\n", "\n", @@ -159,7 +156,7 @@ { "cell_type": "code", "execution_count": null, - "id": "7dbbe4a7-d15c-4a8f-8f51-ac0089870794", + "id": "e388eb93-7a9b-4ae4-91fc-cf32947f63a9", "metadata": {}, "outputs": [], "source": [] diff --git a/examples/scatter.ipynb b/examples/scatter.ipynb index ce028366f..27054aadf 100644 --- a/examples/scatter.ipynb +++ b/examples/scatter.ipynb @@ -12,7 +12,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 14, "id": "9b3041ad-d94e-4b2a-af4d-63bcd19bf6c2", "metadata": { "tags": [] @@ -25,14 +25,14 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 21, "id": "922990b6-24e9-4fa0-977b-6577f9752d84", "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "f878eb6f385f4045bb2d0e6dc48a585d", + "model_id": "2c91040e84e1425fac42c3e548d58293", "version_major": 2, "version_minor": 0 }, @@ -43,18 +43,10 @@ "metadata": {}, "output_type": "display_data" }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/kushal/Insync/kushalkolar@gmail.com/drive/repos/fastplotlib/fastplotlib/layouts/_base.py:142: UserWarning: `center_scene()` not yet implemented for `PerspectiveCamera`\n", - " warn(\"`center_scene()` not yet implemented for `PerspectiveCamera`\")\n" - ] - }, { "data": { "text/html": [ - "
initial snapshot
" + "
initial snapshot
" ], "text/plain": [ "" @@ -66,7 +58,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "76163442cbce4eb598f4b44ed08cb12b", + "model_id": "62af1ff95e37408eaed099aaf6ab72d2", "version_major": 2, "version_minor": 0 }, @@ -74,7 +66,7 @@ "JupyterWgpuCanvas()" ] }, - "execution_count": 2, + "execution_count": 21, "metadata": {}, "output_type": "execute_result" } @@ -108,8 +100,13 @@ " controllers=controllers\n", ")\n", "\n", - "# create a random distribution of 100 xyz coordinates\n", - "dims = (1000, 3)\n", + "# create a random distribution of 10,000 xyz coordinates\n", + "n_points = 10_000\n", + "\n", + "# if you have a good GPU go for 1.2 million points :D \n", + "# this is multiplied by 3\n", + "n_points = 400_000\n", + "dims = (n_points, 3)\n", "\n", "offset = 15\n", "\n", @@ -122,11 +119,10 @@ " ]\n", ")\n", "\n", - "# colors with a numerical mapping for each offset\n", - "colors = np.array(([0] * 1000) + ([1] * 1000) + ([2] * 1000))\n", + "colors = [\"yellow\"] * n_points + [\"cyan\"] * n_points + [\"magenta\"] * n_points\n", "\n", "for subplot in grid_plot:\n", - " subplot.add_scatter(data=cloud, colors=colors, cmap='cool', alpha=0.7, size=3)\n", + " subplot.add_scatter(data=cloud, colors=colors, alpha=0.7, sizes=5)\n", " \n", " subplot.set_axes_visibility(True)\n", " subplot.set_grid_visibility(True)\n", @@ -138,10 +134,60 @@ "grid_plot.show()" ] }, + { + "cell_type": "code", + "execution_count": 22, + "id": "7b912961-f72e-46ef-889f-c03234831059", + "metadata": {}, + "outputs": [], + "source": [ + "grid_plot[0, 1].get_graphics()[0].colors[400_000:600_000] = \"r\"" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "c6085806-c001-4632-ab79-420b4692693a", + "metadata": {}, + "outputs": [], + "source": [ + "grid_plot[0, 1].get_graphics()[0].colors[:100_000:10] = \"blue\"" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "6f416825-df31-4e5d-b66b-07f23b48e7db", + "metadata": {}, + "outputs": [], + "source": [ + "grid_plot[0, 1].get_graphics()[0].colors[800_000:] = \"green\"" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "c0fd611e-73e5-49e6-a25c-9d5b64afa5f4", + "metadata": {}, + "outputs": [], + "source": [ + "grid_plot[0, 1].get_graphics()[0].colors[800_000:, -1] = 0" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "cd390542-3a44-4973-8172-89e5583433bc", + "metadata": {}, + "outputs": [], + "source": [ + "grid_plot[0, 1].get_graphics()[0].data[:400_000] = grid_plot[0, 1].get_graphics()[0].data[800_000:]" + ] + }, { "cell_type": "code", "execution_count": null, - "id": "7fe0b6ab-8b15-4884-80f4-4b298a57df9a", + "id": "fb49930f-b795-4b41-bbc6-014a27c2f463", "metadata": {}, "outputs": [], "source": [] diff --git a/examples/simple.ipynb b/examples/simple.ipynb index d82b8493b..08685bba3 100644 --- a/examples/simple.ipynb +++ b/examples/simple.ipynb @@ -29,7 +29,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "1317a9d044c04706aa6ea66e0866ac15", + "model_id": "d148f92cf3504beca0c872f062aca491", "version_major": 2, "version_minor": 0 }, @@ -43,7 +43,7 @@ { "data": { "text/html": [ - "
initial snapshot
" + "
initial snapshot
" ], "text/plain": [ "" @@ -55,7 +55,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "3d4c1377e9f345dd9826130942bece5d", + "model_id": "c70338248e494f519cb0aaa1540c56f1", "version_major": 2, "version_minor": 0 }, @@ -82,6 +82,82 @@ "plot.show()" ] }, + { + "cell_type": "code", + "execution_count": 3, + "id": "048a96b8-99a8-41b6-89c2-40d87f6bc1ab", + "metadata": {}, + "outputs": [], + "source": [ + "from pygfx import Event" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "4fefe695-d1d8-4207-a267-75fa7b94ea1b", + "metadata": {}, + "outputs": [], + "source": [ + "class CustomEvent(Event):\n", + " def __init__(self, *args, **kwargs):\n", + " super().__init__(\"custom-event\", *args, **kwargs)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "c34c4301-26c5-47cf-b2b7-ab2ab1b3f794", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "wo = plot.get_graphics()[0].world_object\n", + "\n", + "wo.add_event_handler(print, \"custom-event\")" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "be290970-750f-44f9-8fcf-32a54ee1f446", + "metadata": {}, + "outputs": [], + "source": [ + "ce = CustomEvent()" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "c4c26e59-d12f-45a1-bbe3-1c00cd0664ce", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "<__main__.CustomEvent at 0x7f481c126140>" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ce" + ] + }, { "cell_type": "markdown", "id": "1cb03f42-1029-4b16-a16b-35447d9e2955", @@ -99,7 +175,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "536b4be224d9476f96ae854889914cc9", + "model_id": "02a93cf7260f4f4e9d500209c75fe1a1", "version_major": 2, "version_minor": 0 }, @@ -113,7 +189,7 @@ { "data": { "text/html": [ - "
initial snapshot
" + "
initial snapshot
" ], "text/plain": [ "" @@ -125,7 +201,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "b01db232b5c54e4abd50e969d3d19c33", + "model_id": "8cf632b698844607a62f468f3ffbb3f7", "version_major": 2, "version_minor": 0 }, @@ -151,7 +227,7 @@ "# a function to update the image_graphic\n", "def update_data():\n", " new_data = np.random.rand(512, 512)\n", - " image_graphic.update_data(new_data)\n", + " image_graphic.data = new_data\n", "\n", "#add this as an animation function\n", "plot_v.add_animations(update_data)\n", @@ -177,7 +253,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "ca38f46bfd4c43d1906dde1f9868d5f3", + "model_id": "060eb107d9364a63ae77316c5c806f55", "version_major": 2, "version_minor": 0 }, @@ -191,7 +267,7 @@ { "data": { "text/html": [ - "
initial snapshot
" + "
initial snapshot
" ], "text/plain": [ "" @@ -203,7 +279,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "caf889a00e3c445ea0ca79bcb97c045f", + "model_id": "a289907ca50641709e1d6452397e68be", "version_major": 2, "version_minor": 0 }, @@ -224,7 +300,7 @@ "\n", "def update_data_2():\n", " new_data = np.random.rand(512, 512)\n", - " image_2.update_data(new_data)\n", + " image_2.data = new_data\n", "\n", "plot_sync.add_animations(update_data_2)\n", "\n", @@ -248,12 +324,12 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "ad7aa1a192bd4b4e905e11e7d66f64e8", + "model_id": "b9c22ba4b8f2404bad4d13e413b42e5f", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "VBox(children=(JupyterWgpuCanvas(frame_feedback={'index': 107, 'timestamp': 1671240498.6405487, 'localtime': 1…" + "VBox(children=(JupyterWgpuCanvas(frame_feedback={'index': 49, 'timestamp': 1671701574.7427897, 'localtime': 16…" ] }, "metadata": {}, @@ -282,12 +358,12 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "c71e87e4d7d9489ea7b6eb8ecc52e0e7", + "model_id": "91d89f9b21c949f6a98c774b1b860fd5", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "HBox(children=(JupyterWgpuCanvas(frame_feedback={'index': 193, 'timestamp': 1671240501.7765138, 'localtime': 1…" + "HBox(children=(JupyterWgpuCanvas(frame_feedback={'index': 171, 'timestamp': 1671701579.1038444, 'localtime': 1…" ] }, "metadata": {}, @@ -303,19 +379,32 @@ "id": "e7859338-8162-408b-ac72-37e606057045", "metadata": {}, "source": [ - "### 2D line plot" + "### 2D line plot which also shows the color system used for `LineGraphic` and `ScatterGraphic`" ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 1, + "id": "783d0912-8878-4f63-a5d5-d3b59e5a050b", + "metadata": {}, + "outputs": [], + "source": [ + "from fastplotlib import Plot\n", + "from ipywidgets import VBox, HBox\n", + "import numpy as np\n", + "from functools import partial" + ] + }, + { + "cell_type": "code", + "execution_count": 2, "id": "d13f71d3-3003-4e11-82bd-2876013671f7", "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "8ce8011d8eba472099bf2aab5a91befe", + "model_id": "54d90d0985984fc2ba82c025e53373fe", "version_major": 2, "version_minor": 0 }, @@ -329,7 +418,7 @@ { "data": { "text/html": [ - "
initial snapshot
" + "
initial snapshot
" ], "text/plain": [ "" @@ -341,7 +430,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "0dab1bfc2a2640d0a9c24692e2b87099", + "model_id": "ea25aaecfada4dd9abad5331d2da0e1f", "version_major": 2, "version_minor": 0 }, @@ -349,7 +438,7 @@ "JupyterWgpuCanvas()" ] }, - "execution_count": 7, + "execution_count": 2, "metadata": {}, "output_type": "execute_result" } @@ -357,24 +446,249 @@ "source": [ "plot_l = Plot()\n", "\n", - "# create data for a sine wave\n", - "xs = np.linspace(0, 30, 500)\n", + "# linspace, create 500 evenly spaced x values from -10 to 10\n", + "xs = np.linspace(-10, 10, 100)\n", + "# sine wave\n", "ys = np.sin(xs)\n", + "sine = np.dstack([xs, ys])[0]\n", "\n", - "data1 = np.dstack([xs, ys])[0]\n", "\n", - "# and cosine wave\n", + "# cosine wave\n", "ys = np.cos(xs) + 5\n", - "data2 = np.dstack([xs, ys])[0]\n", + "cosine = np.dstack([xs, ys])[0]\n", + "\n", + "# ricker wavelet\n", + "a = 0.5\n", + "ys = (2/(np.sqrt(3*a)*(np.pi**0.25))) * (1 - (xs/a)**2) * np.exp(-0.5*(xs/a)**2) * 2 + 10\n", + "ricker = np.dstack([xs, ys])[0]\n", "\n", "# we can plot multiple things in the same plot\n", "# this is true for any graphic\n", - "plot_l.add_line(data=data1, size=1.5, cmap=\"jet\")\n", - "plot_l.add_line(data=data2, size=7, cmap=\"plasma\")\n", + "\n", + "# plot sine wave, use a single color\n", + "plot_l.add_line(data=sine, size=1.5, colors=\"magenta\")\n", + "\n", + "# you can also use colormaps for lines\n", + "cosine_graphic = plot_l.add_line(data=cosine, size=5, cmap=\"autumn\")\n", + "\n", + "# or a list of colors for each datapoint\n", + "colors = [\"r\"] * 25 + [\"purple\"] * 25 + [\"y\"] * 25 + [\"b\"] * 25\n", + "ricker_graphic = plot_l.add_line(data=ricker, size=5, colors = colors)\n", "\n", "plot_l.show()" ] }, + { + "cell_type": "code", + "execution_count": 3, + "id": "cb0d13ed-ef07-46ff-b19e-eeca4c831037", + "metadata": {}, + "outputs": [], + "source": [ + "# fancy indexing of colors\n", + "cosine_graphic.colors[:5] = \"magenta\"\n", + "cosine_graphic.colors[90:] = \"yellow\"\n", + "cosine_graphic.colors[60] = \"w\"" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "cfa001f6-c640-4f91-beb0-c19b030e503f", + "metadata": {}, + "outputs": [], + "source": [ + "# event handlers on graphic features\n", + "cosine_graphic.colors.add_event_handler(lambda x: print(x))" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "bb8a0f95-0063-4cd4-a117-e3d62c6e120d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "FeatureEvent @ 0x7f60c49444f0\n", + "type: color-changed\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", + " [0., 1., 1., 1.],\n", + " [0., 1., 1., 1.],\n", + " [0., 1., 1., 1.],\n", + " [0., 1., 1., 1.],\n", + " [0., 1., 1., 1.],\n", + " [0., 1., 1., 1.],\n", + " [0., 1., 1., 1.],\n", + " [0., 1., 1., 1.]], dtype=float32)}\n", + "\n" + ] + } + ], + "source": [ + "# more complex\n", + "cosine_graphic.colors[15:50:3] = \"cyan\"" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "d1a4314b-5723-43c7-94a0-b4cbb0e44d60", + "metadata": {}, + "outputs": [], + "source": [ + "cosine_graphic.data[10:50:5, :2] = sine[10:50:5]\n", + "cosine_graphic.data[90:, 1] = 7" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "682db47b-8c7a-4934-9be4-2067e9fb12d5", + "metadata": {}, + "outputs": [], + "source": [ + "cosine_graphic.data[0] = np.array([[-10, 0, 0]])" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "fcba75b7-9a1e-4aae-9dec-715f7f7456c3", + "metadata": {}, + "outputs": [], + "source": [ + "ricker_graphic.present = False" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "763b9943-53a4-4e2a-b47a-4e9e5c9d7be3", + "metadata": {}, + "outputs": [], + "source": [ + "ricker_graphic.present = True" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "0aa2b178-4bb9-4819-a08d-9187ec0e53c0", + "metadata": {}, + "outputs": [], + "source": [ + "def auto_scale(p):\n", + " p.center_scene()\n", + " p.camera.maintain_aspect = False\n", + " width, height, depth = np.ptp(p.scene.get_world_bounding_box(), axis=0)\n", + " p.camera.width = width\n", + " p.camera.height = height\n", + "\n", + " p.controller.distance = 0\n", + " \n", + " p.controller.zoom(0.8 / p.controller.zoom_value)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "64a20a16-75a5-4772-a849-630ade9be4ff", + "metadata": {}, + "outputs": [], + "source": [ + "ricker_graphic.present.add_event_handler(partial(auto_scale, plot_l))" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "fb093046-c94c-4085-86b4-8cd85cb638ff", + "metadata": {}, + "outputs": [], + "source": [ + "ricker_graphic.present = False" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "f05981c3-c768-4631-ae62-6a8407b20c4c", + "metadata": {}, + "outputs": [], + "source": [ + "ricker_graphic.present = True" + ] + }, + { + "cell_type": "markdown", + "id": "071bc152-5594-4679-90c8-002ed12b37cf", + "metadata": {}, + "source": [ + "## `LineGraphic` and `ScatterGraphic` colors support fancy indexing!" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "6ae3e740-1ed1-4df6-bfcd-b64a48f45c8d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\n", + "\n" + ] + } + ], + "source": [ + "# set the color of the first 250 datapoints, with a stepsize of 3\n", + "cosine_graphic.colors[15:50:3] = \"cyan\"\n", + "\n", + "cosine_graphic.colors[:5] = \"magenta\"\n", + "cosine_graphic.colors[90:] = \"yellow\"\n", + "cosine_graphic.colors[60] = \"w\"" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "1933c64b-8286-490b-8159-57f6c25a4923", + "metadata": {}, + "outputs": [], + "source": [ + "cosine_graphic.colors[50:, 2] = 1" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "bb4cde02-8b09-4dac-a041-bed2bfa36cb1", + "metadata": {}, + "outputs": [], + "source": [ + "cosine_graphic.colors[50:, -1] = 0.4" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "0212d062-956a-4133-ac4d-937781f505fb", + "metadata": {}, + "outputs": [], + "source": [ + "cosine_graphic.colors = \"r\"" + ] + }, { "cell_type": "markdown", "id": "2c90862e-2f2a-451f-a468-0cf6b857e87a", @@ -385,14 +699,14 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 29, "id": "9c51229f-13a2-4653-bff3-15d43ddbca7b", "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "c69434bb678e4f16b33af1b1a3a2564e", + "model_id": "0767e2dc0868414baca5754fb724107f", "version_major": 2, "version_minor": 0 }, @@ -414,7 +728,7 @@ { "data": { "text/html": [ - "
initial snapshot
" + "
initial snapshot
" ], "text/plain": [ "" @@ -426,7 +740,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "3868b0eda4e7467a842fb5631cfc0e1a", + "model_id": "09995d983d9c4ca7bc31d820a799a219", "version_major": 2, "version_minor": 0 }, @@ -434,7 +748,7 @@ "JupyterWgpuCanvas()" ] }, - "execution_count": 8, + "execution_count": 29, "metadata": {}, "output_type": "execute_result" } @@ -471,23 +785,20 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 30, "id": "f404a5ea-633b-43f5-87d1-237017bbca2a", "metadata": {}, "outputs": [ { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "cf987464d78248589bfca940f59d7c87", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "VBox(children=(HBox(children=(JupyterWgpuCanvas(frame_feedback={'index': 46, 'timestamp': 1671240495.5535605, …" - ] - }, - "metadata": {}, - "output_type": "display_data" + "ename": "NameError", + "evalue": "name 'plot' is not defined", + "output_type": "error", + "traceback": [ + "\u001B[0;31m---------------------------------------------------------------------------\u001B[0m", + "\u001B[0;31mNameError\u001B[0m Traceback (most recent call last)", + "Input \u001B[0;32mIn [30]\u001B[0m, in \u001B[0;36m\u001B[0;34m()\u001B[0m\n\u001B[0;32m----> 1\u001B[0m row1 \u001B[38;5;241m=\u001B[39m HBox([\u001B[43mplot\u001B[49m\u001B[38;5;241m.\u001B[39mshow(), plot_v\u001B[38;5;241m.\u001B[39mshow(), plot_sync\u001B[38;5;241m.\u001B[39mshow()])\n\u001B[1;32m 2\u001B[0m row2 \u001B[38;5;241m=\u001B[39m HBox([plot_l\u001B[38;5;241m.\u001B[39mshow(), plot_l3d\u001B[38;5;241m.\u001B[39mshow()])\n\u001B[1;32m 4\u001B[0m VBox([row1, row2])\n", + "\u001B[0;31mNameError\u001B[0m: name 'plot' is not defined" + ] } ], "source": [ diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index 69fb66066..a1a2633b9 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -1,70 +1,106 @@ -from typing import Any +from typing import * -import numpy as np import pygfx -from fastplotlib.utils import get_colors, map_labels_to_colors +from ..utils import get_colors +from .features import GraphicFeature, DataFeature, ColorFeature, PresentFeature class Graphic: def __init__( self, data, - colors: np.ndarray = None, - colors_length: int = None, + colors: Any = False, + n_colors: int = None, cmap: str = None, alpha: float = 1.0, name: str = None ): - self.data = data.astype(np.float32) + """ + + 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 colors_length is None: - # colors_length = self.data.shape[0] + 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._set_colors(colors, colors_length, cmap, alpha, ) + self.colors = ColorFeature(parent=self, colors=colors, n_colors=n_colors, alpha=alpha) - def _set_colors(self, colors, colors_length, cmap, alpha): - if colors_length is None: - colors_length = self.data.shape[0] + # different from visible, toggles the Graphic presence in the Scene + # useful for bbox calculations to ignore these Graphics + self.present = PresentFeature(parent=self) - if colors is None and cmap is None: # just white - self.colors = np.vstack([[1., 1., 1., 1.]] * colors_length).astype(np.float32) + valid_features = ["visible"] + for attr_name in self.__dict__.keys(): + attr = getattr(self, attr_name) + if isinstance(attr, GraphicFeature): + valid_features.append(attr_name) - elif (colors is None) and (cmap is not None): - self.colors = get_colors(n_colors=colors_length, cmap=cmap, alpha=alpha) + self._valid_features = tuple(valid_features) - elif (colors is not None) and (cmap is None): - # assume it's already an RGBA array - colors = np.array(colors) - if colors.shape == (1, 4) or colors.shape == (4,): - self.colors = np.vstack([colors] * colors_length).astype(np.float32) - elif colors.ndim == 2 and colors.shape[1] == 4 and colors.shape[0] == colors_length: - self.colors = colors.astype(np.float32) - else: - raise ValueError(f"Colors array must have ndim == 2 and shape of [, 4]") + @property + def world_object(self) -> pygfx.WorldObject: + return self._world_object - elif (colors is not None) and (cmap is not None): - if colors.ndim == 1 and np.issubdtype(colors.dtype, np.integer): - # assume it's a mapping of colors - self.colors = np.array(map_labels_to_colors(colors, cmap, alpha=alpha)).astype(np.float32) + @property + def interact_features(self) -> Tuple[str]: + """The features for this ``Graphic`` that support interaction.""" + return self._valid_features - else: - raise ValueError("Unknown color format") + @property + def visible(self) -> bool: + return self.world_object.visible + + @visible.setter + def visible(self, v): + """Toggle the visibility of this Graphic""" + self.world_object.visible = v @property def children(self) -> pygfx.WorldObject: return self.world_object.children - def update_data(self, data: Any): - pass + def __setattr__(self, key, value): + if hasattr(self, key): + attr = getattr(self, key) + if isinstance(attr, GraphicFeature): + attr._set(value) + return + + super().__setattr__(key, value) def __repr__(self): if self.name is not None: return f"'{self.name}' fastplotlib.{self.__class__.__name__} @ {hex(id(self))}" else: return f"fastplotlib.{self.__class__.__name__} @ {hex(id(self))}" - diff --git a/fastplotlib/graphics/_graphic_attribute.py b/fastplotlib/graphics/_graphic_attribute.py new file mode 100644 index 000000000..b28b04f64 --- /dev/null +++ b/fastplotlib/graphics/_graphic_attribute.py @@ -0,0 +1,3 @@ + + + diff --git a/fastplotlib/graphics/features/__init__.py b/fastplotlib/graphics/features/__init__.py new file mode 100644 index 000000000..2c489c94f --- /dev/null +++ b/fastplotlib/graphics/features/__init__.py @@ -0,0 +1,4 @@ +from ._colors import ColorFeature +from ._data import DataFeature +from ._present import PresentFeature +from ._base import GraphicFeature diff --git a/fastplotlib/graphics/features/_base.py b/fastplotlib/graphics/features/_base.py new file mode 100644 index 000000000..c11f11bf3 --- /dev/null +++ b/fastplotlib/graphics/features/_base.py @@ -0,0 +1,182 @@ +from abc import ABC, abstractmethod +from inspect import getfullargspec +from warnings import warn +from typing import * + +import numpy as np +from pygfx import Buffer + + +class FeatureEvent: + """ + type: -, example: "color-changed" + pick_info: dict in the form: + { + "index": indices where feature data was changed, ``range`` object or List[int], + "world_object": world object the feature belongs to, + "new_values": the new values + } + """ + def __init__(self, type: str, pick_info: dict): + self.type = type + self.pick_info = pick_info + + def __repr__(self): + return f"{self.__class__.__name__} @ {hex(id(self))}\n" \ + f"type: {self.type}\n" \ + f"pick_info: {self.pick_info}\n" + + +class GraphicFeature(ABC): + def __init__(self, parent, data: Any): + self._parent = parent + if isinstance(data, np.ndarray): + data = data.astype(np.float32) + + self._data = data + self._event_handlers = list() + + @property + def feature_data(self): + """graphic feature data managed by fastplotlib, do not modify directly""" + return self._data + + @abstractmethod + def _set(self, value): + pass + + @abstractmethod + def __repr__(self): + pass + + def add_event_handler(self, handler: callable): + """ + Add an event handler. All added event handlers are called when this feature changes. + The `handler` can optionally accept ``FeatureEvent`` as the first and only argument. + The ``FeatureEvent`` only has two attributes, `type` which denotes the type of event + as a str in the form of "-changed", such as "color-changed". + + Parameters + ---------- + handler: callable + a function to call when this feature changes + + """ + if not callable(handler): + raise TypeError("event handler must be callable") + + if handler in self._event_handlers: + warn(f"Event handler {handler} is already registered.") + return + + self._event_handlers.append(handler) + + #TODO: maybe this can be implemented right here in the base class + @abstractmethod + def _feature_changed(self, key: Union[int, slice, Tuple[slice]], new_data: Any): + """Called whenever a feature changes, and it calls all funcs in self._event_handlers""" + pass + + def _call_event_handlers(self, event_data: FeatureEvent): + for func in self._event_handlers: + try: + if len(getfullargspec(func).args) > 0: + func(event_data) + except: + warn(f"Event handler {func} has an unresolvable argspec, trying it anyways.") + func(event_data) + else: + func() + + +def cleanup_slice(key: Union[int, slice], upper_bound) -> Union[slice, int]: + if isinstance(key, int): + return key + + if isinstance(key, tuple): + # if tuple of slice we only need the first obj + # since the first obj is the datapoint indices + if isinstance(key[0], slice): + key = key[0] + else: + raise TypeError("Tuple slicing must have slice object in first position") + + if not isinstance(key, slice): + raise TypeError("Must pass slice or int object") + + start = key.start + stop = key.stop + step = key.step + for attr in [start, stop, step]: + if attr is None: + continue + if attr < 0: + raise IndexError("Negative indexing not supported.") + + if start is None: + start = 0 + + if stop is None: + stop = upper_bound + + elif stop > upper_bound: + raise IndexError("Index out of bounds") + + step = key.step + if step is None: + step = 1 + + return slice(start, stop, step) + + +class GraphicFeatureIndexable(GraphicFeature): + """And indexable Graphic Feature, colors, data, sizes etc.""" + + def _set(self, value): + self[:] = value + + @abstractmethod + def __getitem__(self, item): + pass + + @abstractmethod + def __setitem__(self, key, value): + pass + + @abstractmethod + def _update_range(self, key): + pass + + @property + @abstractmethod + def _buffer(self) -> Buffer: + pass + + @property + def _upper_bound(self) -> int: + return self.feature_data.shape[0] + + def _update_range_indices(self, key): + """Currently used by colors and data""" + key = cleanup_slice(key, self._upper_bound) + + if isinstance(key, int): + self._buffer.update_range(key, size=1) + return + + # else if it's a slice obj + if isinstance(key, slice): + if key.step == 1: # we cleaned up the slice obj so step of None becomes 1 + # update range according to size using the offset + self._buffer.update_range(offset=key.start, size=key.stop - key.start) + + else: + step = key.step + # convert slice to indices + ixs = range(key.start, key.stop, step) + for ix in ixs: + 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 new file mode 100644 index 000000000..f45f99040 --- /dev/null +++ b/fastplotlib/graphics/features/_colors.py @@ -0,0 +1,187 @@ +import numpy as np + +from ._base import GraphicFeatureIndexable, cleanup_slice, FeatureEvent +from pygfx import Color + + +class ColorFeature(GraphicFeatureIndexable): + @property + def _buffer(self): + return self._parent.world_object.geometry.colors + + def __getitem__(self, item): + return self._buffer.data[item] + + def __repr__(self): + return repr(self._buffer.data) + + def __init__(self, parent, colors, n_colors, alpha: float = 1.0): + """ + ColorFeature + + Parameters + ---------- + parent: Graphic or GraphicCollection + + colors: str, array, or iterable + 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 + """ + # if provided as a numpy array of str + if isinstance(colors, np.ndarray): + if colors.dtype.kind in ["U", "S"]: + colors = colors.tolist() + # if the color is provided as a numpy array + if isinstance(colors, np.ndarray): + if colors.shape == (4,): # single RGBA array + data = np.repeat( + np.array([colors]), + n_colors, + axis=0 + ) + # else assume it's already a stack of RGBA arrays, keep this directly as the data + elif colors.ndim == 2: + if colors.shape[1] != 4 and colors.shape[0] != n_colors: + raise ValueError( + "Valid array color arguments must be a single RGBA array or a stack of " + "RGBA arrays for each datapoint in the shape [n_datapoints, 4]" + ) + data = colors + else: + raise ValueError( + "Valid array color arguments must be a single RGBA array or a stack of " + "RGBA arrays for each datapoint in the shape [n_datapoints, 4]" + ) + + # if the color is provided as an iterable + elif isinstance(colors, (list, tuple, np.ndarray)): + # if iterable of str + if all([isinstance(val, str) for val in colors]): + if not len(colors) == n_colors: + raise ValueError( + f"Valid iterable color arguments must be a `tuple` or `list` of `str` " + f"where the length of the iterable is the same as the number of datapoints." + ) + + data = np.vstack([np.array(Color(c)) for c in colors]) + + # if it's a single RGBA array as a tuple/list + elif len(colors) == 4: + c = Color(colors) + data = np.repeat(np.array([c]), n_colors, axis=0) + + else: + raise ValueError( + f"Valid iterable color arguments must be a `tuple` or `list` representing RGBA values or " + f"an iterable of `str` with the same length as the number of datapoints." + ) + else: + # assume it's a single color, use pygfx.Color to parse it + c = Color(colors) + data = np.repeat(np.array([c]), n_colors, axis=0) + + if alpha != 1.0: + data[:, -1] = alpha + + super(ColorFeature, self).__init__(parent, data) + + def __setitem__(self, key, value): + # parse numerical slice indices + if isinstance(key, slice): + _key = cleanup_slice(key, self._upper_bound) + indices = range(_key.start, _key.stop, _key.step) + + # or single numerical index + elif isinstance(key, int): + if key > self._upper_bound: + raise IndexError("Index out of bounds") + indices = [key] + + elif isinstance(key, tuple): + if not isinstance(value, (float, int, np.ndarray)): + raise ValueError( + "If using multiple-fancy indexing for color, you can only set numerical" + "values since this sets the RGBA array data directly." + ) + + if len(key) != 2: + raise ValueError("fancy indexing for colors must be 2-dimension, i.e. [n_datapoints, RGBA]") + + # set the user passed data directly + self._buffer.data[key] = value + + # update range + # first slice obj is going to be the indexing so use key[0] + # key[1] is going to be RGBA so get rid of it to pass to _update_range + # _key = cleanup_slice(key[0], self._upper_bound) + self._update_range(key) + self._feature_changed(key, value) + return + + else: + raise TypeError("Graphic features only support integer and numerical fancy indexing") + + new_data_size = len(indices) + + if not isinstance(value, np.ndarray): + color = np.array(Color(value)) # pygfx color parser + # make it of shape [n_colors_modify, 4] + new_colors = np.repeat( + np.array([color]).astype(np.float32), + new_data_size, + axis=0 + ) + + # if already a numpy array + elif isinstance(value, np.ndarray): + # if a single color provided as numpy array + if value.shape == (4,): + new_colors = value.astype(np.float32) + # if there are more than 1 datapoint color to modify + if new_data_size > 1: + new_colors = np.repeat( + np.array([new_colors]).astype(np.float32), + new_data_size, + axis=0 + ) + + elif value.ndim == 2: + if value.shape[1] != 4 and value.shape[0] != new_data_size: + raise ValueError("numpy array passed to color must be of shape (4,) or (n_colors_modify, 4)") + # if there is a single datapoint to change color of but user has provided shape [1, 4] + if new_data_size == 1: + new_colors = value.ravel().astype(np.float32) + else: + new_colors = value.astype(np.float32) + + else: + raise ValueError("numpy array passed to color must be of shape (4,) or (n_colors_modify, 4)") + + self._buffer.data[key] = new_colors + + self._update_range(key) + self._feature_changed(key, new_colors) + + def _update_range(self, key): + self._update_range_indices(key) + + def _feature_changed(self, key, new_data): + 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) + else: + raise TypeError("feature changed key must be slice or int") + + pick_info = { + "index": indices, + "world_object": self._parent.world_object, + "new_data": new_data, + } + + event_data = FeatureEvent(type="color-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 new file mode 100644 index 000000000..6e1feac2a --- /dev/null +++ b/fastplotlib/graphics/features/_data.py @@ -0,0 +1,79 @@ +from ._base import GraphicFeatureIndexable, cleanup_slice, FeatureEvent +from pygfx import Buffer +from typing import * +from ...utils import fix_data, to_float32 + + +class DataFeature(GraphicFeatureIndexable): + """ + Access to the buffer data being shown in the graphic. + Supports fancy indexing if the data array also does. + """ + # 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) + + @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 + + def __repr__(self): + return repr(self._buffer.data) + + def __getitem__(self, item): + return self._buffer.data[item] + + def __setitem__(self, key, value): + 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) + self._buffer.data[key] = value + self._update_range(key) + + 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) + + def _update_range_grid(self, key): + # image data + self._buffer.update_range((0, 0, 0), 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): + 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) diff --git a/fastplotlib/graphics/features/_present.py b/fastplotlib/graphics/features/_present.py new file mode 100644 index 000000000..fd98dc32f --- /dev/null +++ b/fastplotlib/graphics/features/_present.py @@ -0,0 +1,51 @@ +from ._base import GraphicFeature, FeatureEvent +from pygfx import Scene + + +class PresentFeature(GraphicFeature): + """ + Toggles if the object is present in the scene, different from visible \n + Useful for computing bounding boxes from the Scene to only include graphics + that are present + """ + def __init__(self, parent, present: bool = True): + self._scene = None + super(PresentFeature, self).__init__(parent, present) + + def _set(self, present: bool): + i = 0 + while not isinstance(self._scene, Scene): + self._scene = self._parent.world_object.parent + i += 1 + + if i > 100: + raise RecursionError( + "Exceded scene graph depth threshold, cannot find Scene associated with" + "this graphic." + ) + + if present: + if self._parent.world_object not in self._scene.children: + self._scene.add(self._parent.world_object) + + else: + if self._parent.world_object in self._scene.children: + self._scene.remove(self._parent.world_object) + + self._feature_changed(key=None, new_data=present) + + def __repr__(self): + return repr(self.feature_data) + + 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="present-changed", pick_info=pick_info) + + self._call_event_handlers(event_data) \ No newline at end of file diff --git a/fastplotlib/graphics/features/_sizes.py b/fastplotlib/graphics/features/_sizes.py new file mode 100644 index 000000000..e69de29bb diff --git a/fastplotlib/graphics/histogram.py b/fastplotlib/graphics/histogram.py index 2c846a223..546e481ff 100644 --- a/fastplotlib/graphics/histogram.py +++ b/fastplotlib/graphics/histogram.py @@ -1,4 +1,4 @@ -from _warnings import warn +from warnings import warn from typing import Union, Dict import numpy as np @@ -20,7 +20,7 @@ def __init__( data: np.ndarray = None, bins: Union[int, str] = 'auto', pre_computed: Dict[str, np.ndarray] = None, - colors: np.ndarray = None, + colors: np.ndarray = "w", draw_scale_factor: float = 100.0, draw_bin_width_scale: float = 1.0, **kwargs @@ -82,9 +82,9 @@ def __init__( data = np.vstack([x_positions_bins, self.hist]) - super(HistogramGraphic, self).__init__(data=data, colors=colors, colors_length=n_bins, **kwargs) + super(HistogramGraphic, self).__init__(data=data, colors=colors, n_colors=n_bins, **kwargs) - self.world_object: pygfx.Group = pygfx.Group() + self._world_object: pygfx.Group = pygfx.Group() for x_val, y_val, bin_center in zip(x_positions_bins, self.hist, self.bin_centers): geometry = pygfx.plane_geometry( diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index ddfc43772..77c531c8a 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -67,8 +67,8 @@ def __init__( if (vmin is None) or (vmax is None): vmin, vmax = quick_min_max(data) - self.world_object: pygfx.Image = pygfx.Image( - pygfx.Geometry(grid=pygfx.Texture(self.data, dim=2)), + 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)) ) @@ -79,11 +79,3 @@ def clim(self) -> Tuple[float, float]: @clim.setter def clim(self, levels: Tuple[float, float]): self.world_object.material.clim = levels - - def update_data(self, data: np.ndarray): - self.world_object.geometry.grid.data[:] = data - self.world_object.geometry.grid.update_range((0, 0, 0), self.world_object.geometry.grid.size) - - def update_cmap(self, cmap: str, alpha: float = 1.0): - self.world_object.material.map = get_cmap_texture(name=cmap) - self.world_object.geometry.grid.update_range((0, 0, 0), self.world_object.geometry.grid.size) diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index c2d09bfb4..edf99e43c 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -11,7 +11,7 @@ def __init__( data: Any, z_position: float = 0.0, size: float = 2.0, - colors: np.ndarray = None, + colors: Union[str, np.ndarray, Iterable] = "w", cmap: str = None, *args, **kwargs @@ -30,7 +30,9 @@ def __init__( size: float, optional thickness of the line - colors: + colors: str, array, or iterable + 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 @@ -40,47 +42,20 @@ def __init__( kwargs passed to Graphic """ - super(LineGraphic, self).__init__(data, colors=colors, cmap=cmap, *args, **kwargs) - self.fix_data() + super(LineGraphic, self).__init__(data, colors=colors, cmap=cmap, *args, **kwargs) if size < 1.1: material = pygfx.LineThinMaterial else: material = pygfx.LineMaterial - self.data = np.ascontiguousarray(self.data) + # self.data = np.ascontiguousarray(self.data) - self.world_object: pygfx.Line = pygfx.Line( - geometry=pygfx.Geometry(positions=self.data, colors=self.colors), + 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), material=material(thickness=size, vertex_colors=True) ) self.world_object.position.z = z_position - - def fix_data(self): - # TODO: data should probably be a property of any Graphic?? Or use set_data() and get_data() - if self.data.ndim == 1: - self.data = np.dstack([np.arange(self.data.size), self.data])[0] - - if self.data.shape[1] != 3: - if self.data.shape[1] != 2: - raise ValueError("Must pass 1D, 2D or 3D data") - - # zeros for z - zs = np.zeros(self.data.shape[0], dtype=np.float32) - - self.data = np.dstack([self.data[:, 0], self.data[:, 1], zs])[0] - - def update_data(self, data: np.ndarray): - self.data = data.astype(np.float32) - self.fix_data() - - self.world_object.geometry.positions.data[:] = self.data - self.world_object.geometry.positions.update_range() - - def update_colors(self, colors: np.ndarray): - super(LineGraphic, self)._set_colors(colors=colors, colors_length=self.data.shape[0], cmap=None, alpha=None) - - self.world_object.geometry.colors.data[:] = self.colors - self.world_object.geometry.colors.update_range() diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index b097f8c5a..0ea5a8831 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -1,4 +1,4 @@ -from typing import List +from typing import * import numpy as np import pygfx @@ -7,49 +7,22 @@ class ScatterGraphic(Graphic): - def __init__(self, data: np.ndarray, z_position: float = 0.0, size: int = 1, colors: np.ndarray = None, cmap: str = None, *args, **kwargs): + 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) - if self.data.ndim == 1: - # assume single 3D point - if not self.data.size == 3: - raise ValueError("If passing single you must specify all coordinates, i.e. x, y and z.") - elif self.data.shape[1] != 3: - if self.data.shape[1] == 2: - - # zeros - zs = np.zeros(self.data.shape[0], dtype=np.float32) - - self.data = np.dstack([self.data[:, 0], self.data[:, 1], zs])[0] - if self.data.shape[1] > 3 or self.data.shape[1] < 1: - raise ValueError("Must pass 2D or 3D data or a single point") - - self.world_object: pygfx.Group = pygfx.Group() - self.points_objects: List[pygfx.Points] = list() - - for color in np.unique(self.colors, axis=0): - positions = self._process_positions( - self.data[np.all(self.colors == color, axis=1)] - ) - - points = pygfx.Points( - pygfx.Geometry(positions=positions), - pygfx.PointsMaterial(size=size, color=color) - ) - - self.world_object.add(points) - self.points_objects.append(points) + if isinstance(sizes, int): + sizes = np.full(self.data.feature_data.shape[0], sizes, dtype=np.float32) + elif isinstance(sizes, np.ndarray): + if (sizes.ndim != 1) or (sizes.size != self.data.feature_data.shape[0]): + raise ValueError(f"numpy array of `sizes` must be 1 dimensional with " + f"the same length as the number of datapoints") + elif isinstance(sizes, list): + if len(sizes) != self.data.feature_data.shape[0]: + raise ValueError("list of `sizes` must have the same length as the number of datapoints") + + 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) + ) self.world_object.position.z = z_position - - def _process_positions(self, positions: np.ndarray): - if positions.ndim == 1: - positions = np.array([positions]) - - return positions - - def update_data(self, data: np.ndarray): - positions = self._process_positions(data).astype(np.float32) - - self.points_objects[0].geometry.positions.data[:] = positions - self.points_objects[0].geometry.positions.update_range(positions.shape[0]) diff --git a/fastplotlib/utils/functions.py b/fastplotlib/utils/functions.py index 650cfb053..4df784b6e 100644 --- a/fastplotlib/utils/functions.py +++ b/fastplotlib/utils/functions.py @@ -30,13 +30,7 @@ def _get_cmap(name: str, alpha: float = 1.0) -> np.ndarray: return cmap.astype(np.float32) -def get_colors( - n_colors: int, - cmap: str, - spacing: str = 'uniform', - alpha: float = 1.0 - ) \ - -> List[Union[np.ndarray, str]]: +def get_colors(n_colors: int, cmap: str, alpha: float = 1.0) -> np.ndarray: cmap = _get_cmap(cmap, alpha) cm_ixs = np.linspace(0, 255, n_colors, dtype=int) return np.take(cmap, cm_ixs, axis=0).astype(np.float32) @@ -94,3 +88,36 @@ 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 06c62180a..5fba44d56 100644 --- a/fastplotlib/widgets/image.py +++ b/fastplotlib/widgets/image.py @@ -158,7 +158,7 @@ def current_index(self, index: Dict[str, int]): for i, (ig, data) in enumerate(zip(self.image_graphics, self.data)): frame = self._process_indices(data, self._current_index) frame = self._process_frame_apply(frame, i) - ig.update_data(frame) + ig.data = frame def __init__( self, 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