diff --git a/docs/dev/notebooks/interactive_tools.ipynb b/docs/dev/notebooks/interactive_tools.ipynb
index 721fa2db6ba..a46dc3593c5 100644
--- a/docs/dev/notebooks/interactive_tools.ipynb
+++ b/docs/dev/notebooks/interactive_tools.ipynb
@@ -20,7 +20,7 @@
" \n",
" \n",
@@ -42,6 +42,39 @@
"LetsPlot.setup_html()"
]
},
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "92f5b6fe-8797-4d9a-a8ec-7a0f9e0457cd",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def dump_plot(plot, display=None):\n",
+ " import json\n",
+ "\n",
+ " try:\n",
+ " import clipboard\n",
+ " except:\n",
+ " clipboard = None\n",
+ " \n",
+ " from lets_plot._type_utils import standardize_dict\n",
+ " \n",
+ " plot_dict = standardize_dict(plot.as_dict())\n",
+ " plot_json = json.dumps(plot_dict, indent=2)\n",
+ " \n",
+ " if clipboard:\n",
+ " clipboard.copy('')\n",
+ " clipboard.copy(str(plot_json))\n",
+ " else:\n",
+ " if display is None:\n",
+ " display = True\n",
+ " \n",
+ " if display:\n",
+ " print(plot_json)\n",
+ "\n",
+ " return plot\n"
+ ]
+ },
{
"cell_type": "markdown",
"id": "a0aa16b5-94a6-4691-8e58-48b4cc7f440d",
@@ -52,20 +85,20 @@
},
{
"cell_type": "code",
- "execution_count": 2,
+ "execution_count": 3,
"id": "b6b03e95-28c7-47a9-9b82-0a9a07bfbb88",
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
- "
\n",
+ " \n",
" "
],
"text/plain": [
- ""
+ ""
]
},
- "execution_count": 2,
+ "execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
@@ -142,24 +183,39 @@
},
{
"cell_type": "code",
- "execution_count": 3,
+ "execution_count": 4,
"id": "9dcd1a07-db64-4e80-a76e-23690fa8eaef",
"metadata": {},
"outputs": [],
"source": [
- "countries = geocode_countries().get_boundaries()"
+ "world = geocode_countries().get_boundaries()\n",
+ "north_america = geocode_countries(['usa', 'canada', 'mexico']).get_boundaries()\n",
+ "australia = geocode_countries('australia').get_boundaries(3)\n",
+ "\n",
+ "world_map = ggplot() \\\n",
+ " + geom_map(aes(fill='country'), map=world, size=0.1, show_legend=False, tooltips=layer_tooltips().line(\"@country\"))\n",
+ "\n",
+ "north_america_map = ggplot() \\\n",
+ " + geom_map(aes(fill='country'), map=north_america, size=0.1, show_legend=False,\n",
+ " tooltips=layer_tooltips().line(\"@country\")) \\\n",
+ " + coord_map(xlim=[-180, -50])\n",
+ "\n",
+ "australia_map = ggplot() \\\n",
+ " + geom_map(aes(fill='country'), map=australia, size=0.1, show_legend=False,\n",
+ " tooltips=layer_tooltips().line(\"@country\")) \\\n",
+ " + coord_map(xlim=[110, None], ylim=[-45, None])"
]
},
{
"cell_type": "code",
- "execution_count": 4,
+ "execution_count": 5,
"id": "0b25fcbb-ca2d-488d-963b-3dc123d01555",
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
- " \n",
+ " \n",
" "
],
"text/plain": [
- ""
+ ""
]
},
- "execution_count": 4,
+ "execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
- "ggplot() + geom_map(aes(fill='country'), map=countries, size=0.1, show_legend=False) + coord_map() + ggsize(1000, 800)"
+ "ggplot() + geom_map(aes(fill='country'), map=world, size=0.1, show_legend=False) + coord_map() + ggsize(1000, 800)"
]
},
{
@@ -231,7 +295,7 @@
},
{
"cell_type": "code",
- "execution_count": 5,
+ "execution_count": 6,
"id": "d26ecd00-fbdf-40e9-a3f0-05c921a1cec0",
"metadata": {},
"outputs": [],
@@ -241,14 +305,14 @@
},
{
"cell_type": "code",
- "execution_count": 6,
+ "execution_count": 7,
"id": "59360926-9bcd-4aff-87cd-9ced2c638752",
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
- " \n",
+ " \n",
" "
],
"text/plain": [
- ""
+ ""
]
},
- "execution_count": 6,
+ "execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
@@ -334,14 +406,14 @@
},
{
"cell_type": "code",
- "execution_count": 7,
+ "execution_count": 8,
"id": "8f7fab7c-90db-459c-935a-4c8901b93636",
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
- " \n",
+ " \n",
" "
],
"text/plain": [
- ""
+ ""
]
},
- "execution_count": 7,
+ "execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
@@ -431,111 +511,486 @@
},
{
"cell_type": "markdown",
- "id": "a595b496-ba17-429c-92ba-23c4608ff7e5",
+ "id": "d8a13f9c-7d81-47f9-b3e3-8bca1f160dff",
"metadata": {},
"source": [
- "# Marginal layers"
+ "# Grids"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "id": "3c9f7f20-8ddc-4801-8617-a33f1d27d0d1",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ " \n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 9,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "gggrid([\n",
+ " world_map,\n",
+ " gggrid([\n",
+ " north_america_map,\n",
+ " australia_map\n",
+ " ])\n",
+ "], ncol=1)"
]
},
{
"cell_type": "markdown",
- "id": "026ccabf-a964-42d2-891c-460e07164195",
+ "id": "7ecb327c-2ec0-44ca-85ad-3921c3bc9151",
"metadata": {},
"source": [
- "Not yet supported"
+ "# Bunch"
]
},
{
"cell_type": "code",
- "execution_count": 8,
- "id": "6e36f19f-1bea-478f-a447-ce33672a1b0b",
+ "execution_count": 10,
+ "id": "5e4c5269-6f58-4401-9827-9a2238bc6abb",
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
- " \n",
+ " \n",
" "
- ],
- "text/plain": [
- ""
]
},
- "execution_count": 8,
"metadata": {},
- "output_type": "execute_result"
+ "output_type": "display_data"
}
],
"source": [
- "np.random.seed(0)\n",
- "\n",
+ "bunch = GGBunch()\n",
+ "bunch.add_plot(world_map, 0, 0, 800, 400)\n",
+ "bunch.add_plot(north_america_map, 0, 400, 400, 300)\n",
+ "bunch.add_plot(australia_map, 400, 400, 300, 300)\n",
+ "bunch.show()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a595b496-ba17-429c-92ba-23c4608ff7e5",
+ "metadata": {},
+ "source": [
+ "# Marginal layers"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "026ccabf-a964-42d2-891c-460e07164195",
+ "metadata": {},
+ "source": [
+ "Not yet supported"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "id": "6e36f19f-1bea-478f-a447-ce33672a1b0b",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ " \n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 11,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "np.random.seed(0)\n",
+ "\n",
"cov0=[[1, -.8],\n",
" [-.8, 1]]\n",
"cov1=[[ 10, .1],\n",
@@ -553,6 +1008,1842 @@
"p = ggplot(data, aes(\"x\", \"y\", color=\"c\", fill=\"c\")) + geom_point()\n",
"p + ggmarginal(\"tr\", layer=geom_density(alpha=0.3, show_legend=False))"
]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "bbe4f3d8-d96c-482b-82b3-8eb767426d98",
+ "metadata": {},
+ "source": [
+ "# livemap"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "834e4884-8c08-4cab-abb6-492a3b16d704",
+ "metadata": {},
+ "source": [
+ "## Tooltips precision test (no hitbox simplification, tooltip must appear exactly at borders)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "id": "31458e98-4ef3-4f88-89cd-78686734b4e7",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ " \n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 12,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "x1 = [0, 0, 40, 40, 0]\n",
+ "y1 = [0, 40, 40, 0, 0]\n",
+ "g1 = ['a'] * len(x1)\n",
+ "\n",
+ "x2 = [it + 60 for it in x1]\n",
+ "y2 = y1\n",
+ "g2 = ['b'] * len(x2)\n",
+ "\n",
+ "poly = {\n",
+ " 'x': x1 + x2,\n",
+ " 'y': y1 + y2,\n",
+ " 'g': g1 + g2,\n",
+ "}\n",
+ "\n",
+ "ggplot() \\\n",
+ " + geom_livemap() \\\n",
+ " + geom_polygon(aes(x='x', y='y', fill='g'), data=poly, tooltips=layer_tooltips().line(\"^fill\"))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "b68b642f-5e5e-4fea-ace4-3d8d0d03dcaa",
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "id": "c5113b60-588d-43d6-be8d-abaeb3825323",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ " \n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 13,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "ggplot() + geom_livemap() + geom_polygon(aes(fill='country'), size=0, map=world, show_legend=False)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "id": "af71d61e-7821-4b79-a471-2411c0fbfaf7",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ " \n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 14,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "gggrid([\n",
+ " ggplot() + geom_livemap() + geom_polygon(aes(fill='country'), size=0, map=world, show_legend=False, tooltips=layer_tooltips().line(\"^fill\")),\n",
+ " ggplot() + geom_livemap() + geom_polygon(aes(fill='country'), size=0, map=north_america, show_legend=False, tooltips=layer_tooltips().line(\"^fill\")),\n",
+ "])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3f98dd91-b54b-47a8-bd8b-091e3c562a85",
+ "metadata": {},
+ "source": [
+ "# Performance"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "id": "4f9608e0-95f5-4971-aa61-2f942b190ff5",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ " \n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 15,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "np.random.seed(0)\n",
+ "\n",
+ "cov0=[[1, -.8],\n",
+ " [-.8, 1]]\n",
+ "cov1=[[ 10, .1],\n",
+ " [.1, .1]]\n",
+ "\n",
+ "n = 4000\n",
+ "\n",
+ "x0, y0 = np.random.multivariate_normal(mean=[-2,0], cov=cov0, size=n).T\n",
+ "x1, y1 = np.random.multivariate_normal(mean=[0,1], cov=cov1, size=n).T\n",
+ "\n",
+ "data = dict(\n",
+ " x = np.concatenate((x0,x1)),\n",
+ " y = np.concatenate((y0,y1)),\n",
+ " c = [\"A\"]*n + [\"B\"]*n\n",
+ ")\n",
+ "\n",
+ "p = ggplot(data, aes(\"x\", \"y\", color=\"c\", fill=\"c\")) + geom_point()\n",
+ "#p + ggmarginal(\"tr\", layer=geom_density(alpha=0.3, show_legend=False))\n",
+ "dump_plot(p)\n",
+ "\n",
+ "def plot(n):\n",
+ " n = int(n / 2)\n",
+ " np.random.seed(0)\n",
+ " \n",
+ " cov0=[[1, -.8],\n",
+ " [-.8, 1]]\n",
+ " cov1=[[ 10, .1],\n",
+ " [.1, .1]]\n",
+ "\n",
+ " \n",
+ " x0, y0 = np.random.multivariate_normal(mean=[-2,0], cov=cov0, size=n).T\n",
+ " x1, y1 = np.random.multivariate_normal(mean=[0,1], cov=cov1, size=n).T\n",
+ " \n",
+ " data = dict(\n",
+ " x = np.concatenate((x0,x1)),\n",
+ " y = np.concatenate((y0,y1)),\n",
+ " c = [\"A\"]*n + [\"B\"]*n\n",
+ " )\n",
+ " \n",
+ " return ggplot(data, aes(\"x\", \"y\", color=\"c\", fill=\"c\")) + geom_point() + ggtitle('n=' + str(n * 2))\n",
+ " \n",
+ "plot(2000)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "id": "db25b134-294e-487d-ad48-bf7f4de796a3",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ " \n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 16,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "plot(100)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "id": "4f767310-2cfa-4a4d-a18a-25bd6aacf2fd",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ " \n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 17,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "plot(1000)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "id": "1cd096df-7c29-4c76-9cc1-571c53b48656",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ " \n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 18,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "plot(10_000)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b353fd5b-f447-470f-aaf1-b6de11c44101",
+ "metadata": {},
+ "source": [
+ "# Tooltips"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 19,
+ "id": "2435f3aa-e5fd-4419-bf5b-1e7107779bd8",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "x1 = [0, 0, 40, 40, 0]\n",
+ "y1 = [0, 40, 40, 0, 0]\n",
+ "g1 = ['a'] * len(x1)\n",
+ "\n",
+ "x2 = [60, 80, 100, 60]\n",
+ "y2 = [0, 40, 0, 0]\n",
+ "g2 = ['b'] * len(x2)\n",
+ "\n",
+ "x3 = [-120, -100, -80, -100, -120]\n",
+ "y3 = [ 20, 40, 20, 0, 20]\n",
+ "g3 = ['c'] * len(x3)\n",
+ "\n",
+ "poly = {\n",
+ " 'x': x1 + x2,\n",
+ " 'y': y1 + y2,\n",
+ " 'g': g1 + g2,\n",
+ "}\n",
+ "\n",
+ "p12 = ggplot() \\\n",
+ " + geom_polygon(\n",
+ " aes(\n",
+ " x=x1 + x2, \n",
+ " y=y1 + y2, \n",
+ " fill=g1 + g2\n",
+ " ),\n",
+ " tooltips=layer_tooltips().line(\"^fill\")\n",
+ " ) \\\n",
+ " + coord_fixed()\n",
+ "\n",
+ "p23 = ggplot() \\\n",
+ " + geom_polygon(\n",
+ " aes(x=x2 + x3, y=y2 + y3, fill=g2 + g3), \n",
+ " tooltips=layer_tooltips().line(\"^fill\")\n",
+ " ) \\\n",
+ " + coord_fixed()\n",
+ "\n",
+ "p13 = ggplot() \\\n",
+ " + geom_polygon(\n",
+ " aes(x=x1 + x3, y=y1 + y3, fill=g1 + g3), \n",
+ " tooltips=layer_tooltips().line(\"^fill\")\n",
+ " ) \\\n",
+ " + coord_fixed()\n",
+ "\n",
+ "# livemap\n",
+ "lp12 = ggplot() \\\n",
+ " + geom_livemap() \\\n",
+ " + geom_polygon(\n",
+ " aes(x=x1 + x2, y=y1 + y2, fill=g1 + g2),\n",
+ " tooltips=layer_tooltips().line(\"^fill\")\n",
+ " )\n",
+ "\n",
+ "lp23 = ggplot() \\\n",
+ " + geom_livemap() \\\n",
+ " + geom_polygon(\n",
+ " aes(x=x2 + x3, y=y2 + y3, fill=g2 + g3), \n",
+ " tooltips=layer_tooltips().line(\"^fill\")\n",
+ " )\n",
+ "\n",
+ "lp13 = ggplot() \\\n",
+ " + geom_livemap() \\\n",
+ " + geom_polygon(\n",
+ " aes(x=x1 + x3, y=y1 + y3, fill=g1 + g3), \n",
+ " tooltips=layer_tooltips().line(\"^fill\")\n",
+ " )\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 20,
+ "id": "912def6e-a000-42e8-9ea7-5b001451568f",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ " \n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 20,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "p12"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 21,
+ "id": "938d837d-1aa6-4d99-befd-494a4008348d",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ " \n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 21,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "p23"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b1c6fa14-35da-43cb-b8e9-af3b8571db2e",
+ "metadata": {},
+ "source": [
+ "### gggrid"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 22,
+ "id": "dbbb9eeb-a971-414b-9b8b-1bdc764f3878",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ " \n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 22,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "gggrid([\n",
+ " p12,\n",
+ " gggrid([\n",
+ " p23, p13\n",
+ " ])\n",
+ "], ncol=1)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 23,
+ "id": "65c91034-373c-4f21-b6a4-307966d35847",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ " \n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 23,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "gggrid([\n",
+ " lp12,\n",
+ " gggrid([\n",
+ " lp23, lp13\n",
+ " ])\n",
+ "], ncol=1)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ae6feb9e-1072-4787-bda4-0900fab7e322",
+ "metadata": {},
+ "source": [
+ "### GGBunch"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 24,
+ "id": "d013b6e1-6f73-4309-a8bb-ec42f4b5611c",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ " \n",
+ " "
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "bunch = GGBunch()\n",
+ "\n",
+ "bunch.add_plot(p12, 0, 0, 600, 400)\n",
+ "bunch.add_plot(p23, 600, 400, 600, 400)\n",
+ "bunch.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 25,
+ "id": "255b75a0-78e1-45fe-a225-29b46cb96d18",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ " \n",
+ " "
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "livemap_bunch = GGBunch()\n",
+ "\n",
+ "livemap_bunch.add_plot(lp12, 0, 0, 600, 400)\n",
+ "livemap_bunch.add_plot(lp23, 600, 400, 600, 400)\n",
+ "\n",
+ "livemap_bunch.show()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "15dbec7f-7f09-478d-9d12-2ebd586620ca",
+ "metadata": {},
+ "source": [
+ "### facet_wrap"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 26,
+ "id": "8f8ed64a-a86f-46fc-8d7b-a2c4aba5f367",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ " \n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 26,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "p12 + facet_wrap('fill', ncol=2, nrow=1)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 27,
+ "id": "75e17055-afd7-4de9-8d9e-1d404515c496",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ " \n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 27,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "lp12 + facet_wrap('fill', ncol=2, nrow=1)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 28,
+ "id": "8b8b3790-e3b9-40f9-add9-822e66de9391",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ " \n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 28,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "lp12 + ggtitle(\"Multi\\nline\\ntitle\")"
+ ]
}
],
"metadata": {
@@ -571,7 +2862,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.8.18"
+ "version": "3.8.15"
}
},
"nbformat": 4,
diff --git a/js-package/src/jsMain/kotlin/FigureModelJs.kt b/js-package/src/jsMain/kotlin/FigureModelJs.kt
index 95f505ac0b2..f204a5820c0 100644
--- a/js-package/src/jsMain/kotlin/FigureModelJs.kt
+++ b/js-package/src/jsMain/kotlin/FigureModelJs.kt
@@ -10,6 +10,7 @@ import org.jetbrains.letsPlot.core.spec.Option.Plot.SPEC_OVERRIDE
import org.jetbrains.letsPlot.platf.w3c.jsObject.dynamicFromAnyQ
import org.jetbrains.letsPlot.platf.w3c.jsObject.dynamicObjectToMap
import org.jetbrains.letsPlot.platf.w3c.jsObject.dynamicToAnyQ
+import org.w3c.dom.Element
import org.w3c.dom.HTMLElement
@OptIn(ExperimentalJsExport::class)
@@ -49,6 +50,7 @@ class FigureModelJs internal constructor(
monolithicParameters.width,
monolithicParameters.height,
monolithicParameters.parentElement,
+ monolithicParameters.eventTarget,
monolithicParameters.options
)
@@ -89,5 +91,6 @@ internal class MonolithicParameters(
val width: Double,
val height: Double,
val parentElement: HTMLElement,
+ val eventTarget: Element,
val options: Map
)
\ No newline at end of file
diff --git a/js-package/src/jsMain/kotlin/FigureToHtml.kt b/js-package/src/jsMain/kotlin/FigureToHtml.kt
index cd661fcc4bc..31cad87ad49 100644
--- a/js-package/src/jsMain/kotlin/FigureToHtml.kt
+++ b/js-package/src/jsMain/kotlin/FigureToHtml.kt
@@ -9,6 +9,7 @@ import kotlinx.dom.createElement
import org.jetbrains.letsPlot.commons.geometry.DoubleRectangle
import org.jetbrains.letsPlot.commons.geometry.DoubleVector
import org.jetbrains.letsPlot.commons.geometry.Vector
+import org.jetbrains.letsPlot.commons.registration.CompositeRegistration
import org.jetbrains.letsPlot.commons.registration.Registration
import org.jetbrains.letsPlot.core.canvasFigure.CanvasFigure
import org.jetbrains.letsPlot.core.interact.event.ToolEventDispatcher
@@ -35,7 +36,8 @@ import org.w3c.dom.svg.SVGSVGElement
internal class FigureToHtml(
private val buildInfo: FigureBuildInfo,
- private val containerElement: HTMLElement
+ private val containerElement: HTMLElement,
+ private val eventTarget: Element
) {
private val parentElement: HTMLElement = if (buildInfo.isComposite) {
@@ -52,6 +54,10 @@ internal class FigureToHtml(
fun eval(): Result {
val buildInfo = buildInfo.layoutedByOuterSize()
+ containerElement.style.apply {
+ width = "${buildInfo.layoutInfo.figureSize.x}px"
+ height = "${buildInfo.layoutInfo.figureSize.y}px"
+ }
buildInfo.injectLiveMapProvider { tiles: List>, spec: Map ->
val cursorServiceConfig = CursorServiceConfig()
@@ -60,20 +66,23 @@ internal class FigureToHtml(
}
val svgRoot = buildInfo.createSvgRoot()
- val toolEventDispatcher = if (svgRoot is CompositeFigureSvgRoot) {
+ val (toolEventDispatcher, eventsRegistration) = if (svgRoot is CompositeFigureSvgRoot) {
processCompositeFigure(
svgRoot,
origin = null, // The topmost SVG
- parentElement = parentElement
+ parentElement = parentElement,
+ eventTarget = eventTarget
)
} else {
processPlotFigure(
- svgRoot as PlotSvgRoot,
- parentElement = parentElement
+ svgRoot = svgRoot as PlotSvgRoot,
+ parentElement = parentElement,
+ eventTarget = eventTarget,
+ eventArea = buildInfo.bounds
)
}
- val registration = object : Registration() {
+ val domCleanupRegistration = object : Registration() {
override fun doRemove() {
while (containerElement.firstChild != null) {
containerElement.removeChild(containerElement.firstChild!!)
@@ -83,11 +92,14 @@ internal class FigureToHtml(
return Result(
toolEventDispatcher,
- registration
+ CompositeRegistration().add(
+ eventsRegistration,
+ domCleanupRegistration
+ )
)
}
- class Result(
+ data class Result(
val toolEventDispatcher: ToolEventDispatcher,
val figureRegistration: Registration
)
@@ -96,10 +108,12 @@ internal class FigureToHtml(
private fun processPlotFigure(
svgRoot: PlotSvgRoot,
parentElement: HTMLElement,
- ): ToolEventDispatcher {
+ eventTarget: Element,
+ eventArea: DoubleRectangle
+ ): Pair {
val plotContainer = PlotContainer(svgRoot)
- val rootSVG: SVGSVGElement = buildPlotFigureSVG(plotContainer, parentElement)
+ val (rootSVG, cleanupRegistration) = buildPlotFigureSVG(plotContainer, parentElement, eventTarget, eventArea)
rootSVG.style.setCursor(CssCursor.CROSSHAIR)
// Livemap cursor pointer
@@ -110,14 +124,15 @@ internal class FigureToHtml(
}
parentElement.appendChild(rootSVG)
- return plotContainer.toolEventDispatcher
+ return plotContainer.toolEventDispatcher to cleanupRegistration
}
private fun processCompositeFigure(
svgRoot: CompositeFigureSvgRoot,
origin: DoubleVector?,
parentElement: HTMLElement,
- ): ToolEventDispatcher {
+ eventTarget: Element,
+ ): Pair {
svgRoot.ensureContentBuilt()
val rootSvgSvg: SvgSvgElement = svgRoot.svg
@@ -151,15 +166,17 @@ internal class FigureToHtml(
}
processPlotFigure(
svgRoot = figureSvgRoot,
- parentElement = figureContainer
+ parentElement = figureContainer,
+ eventTarget = eventTarget,
+ eventArea = figureSvgRoot.bounds.add(origin)
)
} else {
figureSvgRoot as CompositeFigureSvgRoot
- processCompositeFigure(figureSvgRoot, elementOrigin, parentElement)
+ processCompositeFigure(figureSvgRoot, elementOrigin, parentElement, eventTarget)
}
}
- return UnsupportedToolEventDispatcher()
+ return UnsupportedToolEventDispatcher() to Registration.EMPTY
}
fun setupRootHTMLElement(element: HTMLElement, size: DoubleVector) {
@@ -187,9 +204,10 @@ internal class FigureToHtml(
private fun buildPlotFigureSVG(
plotContainer: PlotContainer,
- parentElement: Element
- ): SVGSVGElement {
-
+ parentElement: Element,
+ eventTarget: Element,
+ eventArea: DoubleRectangle,
+ ): Pair {
val svg: SVGSVGElement = mapSvgToSVG(plotContainer.svg)
if (plotContainer.isLiveMap) {
@@ -198,7 +216,12 @@ internal class FigureToHtml(
}
}
- plotContainer.mouseEventPeer.addEventSource(DomMouseEventMapper(svg))
+ val plotMouseEventMapper = DomMouseEventMapper(eventTarget, eventArea)
+
+ val eventsRegistration = CompositeRegistration()
+ eventsRegistration.add(Registration.from(plotMouseEventMapper))
+
+ plotContainer.mouseEventPeer.addEventSource(plotMouseEventMapper)
plotContainer.liveMapFigures.forEach { liveMapFigure ->
val bounds = (liveMapFigure as CanvasFigure).bounds().get()
@@ -211,18 +234,19 @@ internal class FigureToHtml(
setPosition(CssPosition.RELATIVE)
}
+ val canvasMouseEventMapper = DomMouseEventMapper(
+ eventTarget,
+ DoubleRectangle(
+ eventArea.origin.add(bounds.origin.toDoubleVector()),
+ bounds.dimension.toDoubleVector()
+ )
+ )
+ eventsRegistration.add(Registration.from(canvasMouseEventMapper))
+
val canvasControl = DomCanvasControl(
myRootElement = liveMapDiv,
size = Vector(bounds.dimension.x, bounds.dimension.y),
- mouseEventSource = DomMouseEventMapper(
- eventSource = svg,
- bounds = DoubleRectangle.XYWH(
- bounds.origin.x,
- bounds.origin.y,
- bounds.dimension.x,
- bounds.dimension.y
- )
- )
+ mouseEventSource = canvasMouseEventMapper
)
val liveMapReg = liveMapFigure.mapToCanvas(canvasControl)
@@ -231,10 +255,10 @@ internal class FigureToHtml(
liveMapDiv.onDisconnect(liveMapReg::dispose)
}
- return svg
+ return svg to eventsRegistration
}
- private fun HTMLElement.onDisconnect(onDisconnected: () -> Unit): Int {
+ private fun Node.onDisconnect(onDisconnected: () -> Unit): Int {
fun checkConnection() {
if (!isConnected) {
onDisconnected()
diff --git a/js-package/src/jsMain/kotlin/MonolithicJs.kt b/js-package/src/jsMain/kotlin/MonolithicJs.kt
index 16c72ac9be3..0bd83fe8373 100644
--- a/js-package/src/jsMain/kotlin/MonolithicJs.kt
+++ b/js-package/src/jsMain/kotlin/MonolithicJs.kt
@@ -5,6 +5,7 @@
/* root package */
+import kotlinx.browser.document
import org.jetbrains.letsPlot.commons.geometry.DoubleRectangle
import org.jetbrains.letsPlot.commons.geometry.DoubleVector
import org.jetbrains.letsPlot.commons.logging.PortableLogging
@@ -15,9 +16,7 @@ import org.jetbrains.letsPlot.core.util.MonolithicCommon
import org.jetbrains.letsPlot.core.util.MonolithicCommon.PlotsBuildResult.Error
import org.jetbrains.letsPlot.core.util.MonolithicCommon.PlotsBuildResult.Success
import org.jetbrains.letsPlot.platf.w3c.jsObject.dynamicObjectToMap
-import org.w3c.dom.HTMLElement
-import org.w3c.dom.HTMLParagraphElement
-import org.w3c.dom.get
+import org.w3c.dom.*
import sizing.SizingOption
import sizing.SizingPolicy
@@ -50,7 +49,11 @@ fun buildPlotFromRawSpecs(
} else {
emptyMap()
}
- buildPlotFromProcessedSpecsIntern(processedSpec, width, height, parentElement, options)
+
+ val persistentDiv = document.createElement("div") as HTMLDivElement
+
+ parentElement.appendChild(persistentDiv)
+ buildPlotFromProcessedSpecsIntern(processedSpec, width, height, persistentDiv, persistentDiv, options)
} catch (e: RuntimeException) {
handleException(e, parentElement)
null
@@ -83,7 +86,12 @@ fun buildPlotFromProcessedSpecs(
} else {
emptyMap()
}
- buildPlotFromProcessedSpecsIntern(processedSpec, width, height, parentElement, options)
+
+ val persistentDiv = document.createElement("div") as HTMLDivElement
+
+ parentElement.appendChild(persistentDiv)
+
+ buildPlotFromProcessedSpecsIntern(processedSpec, width, height, persistentDiv, persistentDiv, options)
} catch (e: RuntimeException) {
handleException(e, parentElement)
null
@@ -95,6 +103,7 @@ internal fun buildPlotFromProcessedSpecsIntern(
width: Double,
height: Double,
parentElement: HTMLElement,
+ eventTarget: Element,
options: Map
): FigureModelJs? {
@@ -145,23 +154,27 @@ internal fun buildPlotFromProcessedSpecsIntern(
val figureModel = if (success.buildInfos.size == 1) {
// a single figure
val buildInfo = success.buildInfos[0]
- val result = FigureToHtml(buildInfo, parentElement).eval()
+ val result = FigureToHtml(buildInfo, parentElement, eventTarget).eval()
FigureModelJs(
plotSpec,
- MonolithicParameters(width, height, parentElement, options),
+ MonolithicParameters(width, height, parentElement, eventTarget, options),
result.toolEventDispatcher,
result.figureRegistration
)
} else {
// a bunch
- buildGGBunchComponent(success.buildInfos, parentElement)
+ buildGGBunchComponent(success.buildInfos, parentElement, eventTarget)
null
}
return figureModel
}
-fun buildGGBunchComponent(plotInfos: List, parentElement: HTMLElement) {
+fun buildGGBunchComponent(
+ plotInfos: List,
+ parentElement: HTMLElement,
+ eventTarget: Element
+) {
val bunchBounds = plotInfos.map { it.bounds }
.fold(DoubleRectangle(DoubleVector.ZERO, DoubleVector.ZERO)) { acc, bounds ->
acc.union(bounds)
@@ -179,7 +192,8 @@ fun buildGGBunchComponent(plotInfos: List, parentElement: HTMLE
FigureToHtml(
buildInfo = plotInfo,
- containerElement = itemContainerElement
+ containerElement = itemContainerElement,
+ eventTarget = eventTarget
).eval()
}
diff --git a/platf-w3c/src/jsMain/kotlin/org/jetbrains/letsPlot/platf/w3c/mapping/dom/DomMouseEventMapper.kt b/platf-w3c/src/jsMain/kotlin/org/jetbrains/letsPlot/platf/w3c/mapping/dom/DomMouseEventMapper.kt
index 82ea3ce6943..8e36e2df6d6 100644
--- a/platf-w3c/src/jsMain/kotlin/org/jetbrains/letsPlot/platf/w3c/mapping/dom/DomMouseEventMapper.kt
+++ b/platf-w3c/src/jsMain/kotlin/org/jetbrains/letsPlot/platf/w3c/mapping/dom/DomMouseEventMapper.kt
@@ -1,22 +1,24 @@
/*
- * Copyright (c) 2023. JetBrains s.r.o.
+ * Copyright (c) 2024. JetBrains s.r.o.
* Use of this source code is governed by the MIT license that can be found in the LICENSE file.
*/
package org.jetbrains.letsPlot.core.platf.dom
import kotlinx.browser.document
+import kotlinx.browser.window
import org.jetbrains.letsPlot.commons.event.*
import org.jetbrains.letsPlot.commons.geometry.DoubleRectangle
import org.jetbrains.letsPlot.commons.geometry.DoubleVector
import org.jetbrains.letsPlot.commons.intern.observable.event.EventHandler
+import org.jetbrains.letsPlot.commons.registration.CompositeRegistration
+import org.jetbrains.letsPlot.commons.registration.Disposable
import org.jetbrains.letsPlot.commons.registration.Registration
-import org.jetbrains.letsPlot.core.platf.dom.DomEventUtil.getButton
-import org.jetbrains.letsPlot.core.platf.dom.DomEventUtil.getModifiers
import org.jetbrains.letsPlot.platf.w3c.dom.events.DomEventType
import org.jetbrains.letsPlot.platf.w3c.dom.on
import org.w3c.dom.Element
import org.w3c.dom.events.WheelEvent
+import kotlin.math.roundToInt
typealias DomMouseEvent = org.w3c.dom.events.MouseEvent
typealias DomWheelEvent = WheelEvent
@@ -24,37 +26,56 @@ typealias DomWheelEvent = WheelEvent
private const val ENABLE_DEBUG_LOG = false
class DomMouseEventMapper(
- private val eventSource: Element,
- private val bounds: DoubleRectangle? = null,
-) : MouseEventSource {
+ private val eventTarget: Element,
+
+ // The area where the events are handled.
+ // The area is relative to the top-left corner of the event target.
+ private val eventArea: DoubleRectangle = DoubleRectangle.XYWH(
+ x = 0,
+ y = 0,
+ width = eventTarget.clientWidth.toDouble(),
+ height = eventTarget.clientHeight.toDouble()
+ )
+) : MouseEventSource, Disposable {
+
+ private val regs = CompositeRegistration()
private val mouseEventPeer = MouseEventPeer()
- private var state: MouseState = HoverState()
+ private var state: MouseState = MouseOverState()
set(value) {
if (ENABLE_DEBUG_LOG) {
- println("state($${this@DomMouseEventMapper.hashCode()}): ${field::class.simpleName} -> ${value::class.simpleName}")
+ println(
+ "state($${
+ this@DomMouseEventMapper.hashCode().toString(36)
+ }): ${field::class.simpleName} -> ${value::class.simpleName}"
+ )
}
field = value
}
init {
- handle(DomEventType.CLICK) { state.onMouseEvent(DomEventType.CLICK, it) }
- handle(DomEventType.DOUBLE_CLICK) { state.onMouseEvent(DomEventType.DOUBLE_CLICK, it) }
- handle(DomEventType.MOUSE_ENTER) { state.onMouseEvent(DomEventType.MOUSE_ENTER, it) }
- handle(DomEventType.MOUSE_LEAVE) { state.onMouseEvent(DomEventType.MOUSE_LEAVE, it) }
- handle(DomEventType.MOUSE_DOWN) { state.onMouseEvent(DomEventType.MOUSE_DOWN, it) }
- handle(DomEventType.MOUSE_UP) { state.onMouseEvent(DomEventType.MOUSE_UP, it) }
- handle(DomEventType.MOUSE_MOVE) { state.onMouseEvent(DomEventType.MOUSE_MOVE, it) }
- handle(DomEventType.MOUSE_WHEEL) { state.onMouseEvent(DomEventType.MOUSE_WHEEL, it) }
+ if (ENABLE_DEBUG_LOG) {
+ println("DomMouseEventMapper(${this.hashCode().toString(36)}): subarea=$eventArea")
+ }
+
+ fun addHandler(eventSpec: DomEventType) {
+ eventTarget
+ .on(eventSpec, consumer = { mouseEvent -> state.onMouseEvent(eventSpec, mouseEvent) })
+ .also(regs::add)
+ }
+
+ addHandler(DomEventType.CLICK)
+ addHandler(DomEventType.DOUBLE_CLICK)
+ addHandler(DomEventType.MOUSE_ENTER)
+ addHandler(DomEventType.MOUSE_LEAVE)
+ addHandler(DomEventType.MOUSE_DOWN)
+ addHandler(DomEventType.MOUSE_UP)
+ addHandler(DomEventType.MOUSE_MOVE)
+ addHandler(DomEventType.MOUSE_WHEEL)
}
- private fun handle(eventSpec: DomEventType, handler: (DomMouseEvent) -> Unit) {
- eventSource.on(eventSpec, consumer = {
- val needHandle = bounds?.contains(DoubleVector(it.offsetX, it.offsetY)) ?: true
- if (needHandle) {
- handler(it)
- }
- })
+ override fun addEventHandler(eventSpec: MouseEventSpec, eventHandler: EventHandler): Registration {
+ return mouseEventPeer.addEventHandler(eventSpec, eventHandler)
}
private fun dispatch(eventSpec: MouseEventSpec, domMouseEvent: DomMouseEvent) {
@@ -70,15 +91,11 @@ class DomMouseEventMapper(
// Now dragging outside the iframe can be tested.
//domMouseEvent.preventDefault() // Fix for Safari to prevent selection when user drags outside a canvas
- val targetClientOrigin = eventSource.getBoundingClientRect().let { DoubleVector(it.x, it.y) }
- val targetAbsoluteOrigin = bounds?.origin ?: DoubleVector.ZERO
- val eventClientCoord = DoubleVector(domMouseEvent.clientX.toDouble(), domMouseEvent.clientY.toDouble())
- val eventTargetCoord = eventClientCoord.subtract(targetClientOrigin).subtract(targetAbsoluteOrigin)
-
- val x = eventTargetCoord.x.toInt()
- val y = eventTargetCoord.y.toInt()
- val button = getButton(domMouseEvent)
- val modifiers = getModifiers(domMouseEvent)
+ val coord = toEventTargetOffsetCoord(domMouseEvent).subtract(eventArea.origin)
+ val x = coord.x.roundToInt()//domMouseEvent.regionX.roundToInt()
+ val y = coord.y.roundToInt()//domMouseEvent.regionY.roundToInt()
+ val button = DomEventUtil.getButton(domMouseEvent)
+ val modifiers = DomEventUtil.getModifiers(domMouseEvent)
val mouseEvent = when (domMouseEvent) {
is WheelEvent -> MouseWheelEvent(x, y, button, modifiers, domMouseEvent.deltaY)
@@ -92,9 +109,25 @@ class DomMouseEventMapper(
}
}
+ private fun inEventArea(e: DomMouseEvent): Boolean {
+ return toEventTargetOffsetCoord(e) in eventArea
+ }
+
+ // Convert event coordinates to the local coordinate system of the event target
+ // (i.e. the top-left corner of the event target is (0, 0))
+ // Can't use offsetX/Y because it's relative to the element under the mouse pointer, not the event target.
+ // This means that with gggrid even when the mouse is over the most right cell, offsetX is still
+ // in range (0, cell.width), not (cell.left, cell.right)
+ private fun toEventTargetOffsetCoord(e: DomMouseEvent): DoubleVector {
+ val offsetX = e.pageX - window.pageXOffset - eventTarget.getBoundingClientRect().left
+ val offsetY = e.pageY - window.pageYOffset - eventTarget.getBoundingClientRect().top
+
+ return DoubleVector(offsetX, offsetY)
+ }
+
private abstract inner class MouseState {
fun onMouseEvent(type: DomEventType, e: DomMouseEvent) {
- log(type.name)
+ log("${type.name} at (${e.x}, ${e.y})")
handleEvent(type, e)
}
@@ -102,64 +135,83 @@ class DomMouseEventMapper(
fun log(str: String) {
if (ENABLE_DEBUG_LOG) {
- println("${this::class.simpleName}(${this@DomMouseEventMapper.hashCode()}): $str")
+ println("${this::class.simpleName}(${this@DomMouseEventMapper.hashCode().toString(36)}): $str")
}
}
}
- private inner class HoverState : MouseState() {
+ private inner class MouseOutsideState : MouseState() {
override fun handleEvent(type: DomEventType, e: DomMouseEvent) {
- if (type == DomEventType.MOUSE_DOWN) {
- dispatch(MouseEventSpec.MOUSE_PRESSED, e)
- state = ButtonDownState(eventCoord = DoubleVector(e.x, e.y))
- return
+ if (!inEventArea(e)) return
+
+ // mouseover with already pressed button -> drag from another element -> not hover for this element
+ if (e.buttons > 0) return
+
+ when (type) {
+ DomEventType.MOUSE_ENTER, DomEventType.MOUSE_MOVE -> {
+ dispatch(MouseEventSpec.MOUSE_ENTERED, e)
+ state = MouseOverState()
+ }
+ // Ignore buttons/leave events
+ }
+ }
+ }
+
+ private inner class MouseOverState : MouseState() {
+ override fun handleEvent(type: DomEventType, e: DomMouseEvent) {
+ if (!inEventArea(e)) {
+ dispatch(MouseEventSpec.MOUSE_LEFT, e)
+ state = MouseOutsideState()
}
- // Any event with already pressed button -> drag from another element -> ignore events until buttons release
- if (e.buttons > 0) {
- state = ForeignDragging()
+ if (type == DomEventType.MOUSE_DOWN) {
+ dispatch(MouseEventSpec.MOUSE_PRESSED, e)
+ state = MousePressedState(eventCoord = DoubleVector(e.x, e.y))
return
}
when (type) {
DomEventType.MOUSE_MOVE -> dispatch(MouseEventSpec.MOUSE_MOVED, e)
- DomEventType.MOUSE_LEAVE -> dispatch(MouseEventSpec.MOUSE_LEFT, e)
- DomEventType.MOUSE_ENTER -> dispatch(MouseEventSpec.MOUSE_ENTERED, e)
+
+ DomEventType.MOUSE_LEAVE -> {
+ dispatch(MouseEventSpec.MOUSE_LEFT, e)
+ state = MouseOutsideState()
+ }
+
DomEventType.DOUBLE_CLICK -> dispatch(MouseEventSpec.MOUSE_DOUBLE_CLICKED, e) // wish can handle in ButtonDownState
DomEventType.MOUSE_WHEEL -> dispatch(MouseEventSpec.MOUSE_WHEEL_ROTATED, e as DomWheelEvent)
+ DomEventType.MOUSE_ENTER -> {} // should be handled by OutsideState
DomEventType.MOUSE_UP, DomEventType.CLICK -> {} // ignore to prevent ghost clicks on UI
}
}
}
- private inner class ButtonDownState(
+ private inner class MousePressedState(
private val eventCoord: DoubleVector,
private val draggingTriggerDistance: Double = 3.0
) : MouseState() {
override fun handleEvent(type: DomEventType, e: DomMouseEvent) {
when (type) {
- DomEventType.MOUSE_UP -> {
- dispatch(MouseEventSpec.MOUSE_RELEASED, e)
- }
+ DomEventType.MOUSE_UP -> dispatch(MouseEventSpec.MOUSE_RELEASED, e)
// It's safe to set HoverState on CLICK as DOM raises CLICK event exactly after MOUSE_UP,
DomEventType.CLICK -> {
dispatch(MouseEventSpec.MOUSE_CLICKED, e)
- state = HoverState()
+ state = MouseOverState()
}
DomEventType.MOUSE_MOVE -> {
if (DoubleVector(e.x, e.y).subtract(eventCoord).length() > draggingTriggerDistance) {
dispatch(MouseEventSpec.MOUSE_DRAGGED, e)
- state = Dragging()
+ state = MouseDragState()
}
}
}
}
}
- private inner class Dragging : MouseState() {
+ private inner class MouseDragState : MouseState() {
private var myDocumentMouseEventsRegistration = Registration.from(
document.on(DomEventType.MOUSE_MOVE, ::onDocumentMouseMove),
document.on(DomEventType.MOUSE_UP, ::onDocumentMouseUp)
@@ -171,7 +223,7 @@ class DomMouseEventMapper(
private fun onDocumentMouseUp(e: DomMouseEvent) {
dispatch(MouseEventSpec.MOUSE_RELEASED, e)
- state = HoverState()
+ state = MouseOverState()
myDocumentMouseEventsRegistration.dispose()
}
@@ -180,17 +232,7 @@ class DomMouseEventMapper(
}
}
- private inner class ForeignDragging : MouseState() {
- override fun handleEvent(type: DomEventType, e: DomMouseEvent) {
- if (e.buttons > 0) {
- return
- }
-
- state = HoverState()
- }
- }
-
- override fun addEventHandler(eventSpec: MouseEventSpec, eventHandler: EventHandler): Registration {
- return mouseEventPeer.addEventHandler(eventSpec, eventHandler)
+ override fun dispose() {
+ regs.dispose()
}
-}
\ No newline at end of file
+}
diff --git a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/interact/DrawRectFeedback.kt b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/interact/DrawRectFeedback.kt
index 05256370836..2f41775f50a 100644
--- a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/interact/DrawRectFeedback.kt
+++ b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/interact/DrawRectFeedback.kt
@@ -92,6 +92,8 @@ class DrawRectFeedback(
onCompleted = {
val (target, dragFrom, dragTo, _) = it
+ it.reset()
+
val selection = getSelection(dragFrom, dragTo, target)
if (isSelectionAcceptable(selection)) {
@@ -101,8 +103,6 @@ class DrawRectFeedback(
decorationsLayer.children().remove(dragRectSvg)
decorationsLayer.children().remove(selectionSvg)
-
- it.reset()
},
onAborted = {
it.reset()
diff --git a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/interact/PanGeomFeedback.kt b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/interact/PanGeomFeedback.kt
index 9dc266378da..c935626f662 100644
--- a/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/interact/PanGeomFeedback.kt
+++ b/plot-base/src/commonMain/kotlin/org/jetbrains/letsPlot/core/interact/PanGeomFeedback.kt
@@ -31,8 +31,9 @@ class PanGeomFeedback(
val viewport = InteractionUtil.viewportFromTransform(target.geomBounds, translate = dragDelta)
val dataBounds = target.applyViewport(viewport)
- onCompleted(dataBounds)
it.reset()
+
+ onCompleted(dataBounds)
},
onAborted = {
println("PanGeomFeedback abort.")