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,
|