From 4ba7b238e22ac042de14a6b69aa9d61536ddffba Mon Sep 17 00:00:00 2001 From: aliabid94 Date: Wed, 7 Aug 2024 19:17:14 -0700 Subject: [PATCH] Improve plot guide, add double clicking to plots (#9064) * changes * add changeset * chages * changes * changes * changes * changes --------- Co-authored-by: Ali Abid Co-authored-by: gradio-pr-bot --- .changeset/ripe-grapes-refuse.md | 6 +++++ demo/plot_guide_filters_events/data.py | 13 +++++++++++ demo/plot_guide_filters_events/run.ipynb | 1 + demo/plot_guide_filters_events/run.py | 23 +++++++++++++++++++ demo/plot_guide_selection/data.py | 10 ++++++++ demo/plot_guide_selection/run.ipynb | 1 + demo/plot_guide_selection/run.py | 15 ++++++++++++ demo/plot_guide_zoom/data.py | 10 ++++++++ demo/plot_guide_zoom/run.ipynb | 1 + demo/plot_guide_zoom/run.py | 15 ++++++++++++ demo/plot_guide_zoom_sync/data.py | 10 ++++++++ demo/plot_guide_zoom_sync/run.ipynb | 1 + demo/plot_guide_zoom_sync/run.py | 18 +++++++++++++++ gradio/components/native_plot.py | 2 +- gradio/events.py | 3 +++ gradio/hash_seed.txt | 1 + .../01_creating-plots.md | 17 ++++++++++++++ .../02_time-plots.md | 6 ++--- .../03_filters-tables-and-stats.md | 8 +++++-- js/nativeplot/Index.svelte | 14 +++++++++++ 20 files changed, 169 insertions(+), 6 deletions(-) create mode 100644 .changeset/ripe-grapes-refuse.md create mode 100644 demo/plot_guide_filters_events/data.py create mode 100644 demo/plot_guide_filters_events/run.ipynb create mode 100644 demo/plot_guide_filters_events/run.py create mode 100644 demo/plot_guide_selection/data.py create mode 100644 demo/plot_guide_selection/run.ipynb create mode 100644 demo/plot_guide_selection/run.py create mode 100644 demo/plot_guide_zoom/data.py create mode 100644 demo/plot_guide_zoom/run.ipynb create mode 100644 demo/plot_guide_zoom/run.py create mode 100644 demo/plot_guide_zoom_sync/data.py create mode 100644 demo/plot_guide_zoom_sync/run.ipynb create mode 100644 demo/plot_guide_zoom_sync/run.py create mode 100644 gradio/hash_seed.txt diff --git a/.changeset/ripe-grapes-refuse.md b/.changeset/ripe-grapes-refuse.md new file mode 100644 index 0000000000..2df05528ad --- /dev/null +++ b/.changeset/ripe-grapes-refuse.md @@ -0,0 +1,6 @@ +--- +"@gradio/nativeplot": minor +"gradio": minor +--- + +feat:Improve plot guide, add double clicking to plots diff --git a/demo/plot_guide_filters_events/data.py b/demo/plot_guide_filters_events/data.py new file mode 100644 index 0000000000..e9c8e087c3 --- /dev/null +++ b/demo/plot_guide_filters_events/data.py @@ -0,0 +1,13 @@ +import pandas as pd +import numpy as np +import random +from datetime import datetime, timedelta + +now = datetime.now() + +df = pd.DataFrame({ + 'time': [now - timedelta(minutes=5*i) for i in range(25)], + 'price': np.random.randint(100, 1000, 25), + 'origin': [random.choice(["DFW", "DAL", "HOU"]) for _ in range(25)], + 'destination': [random.choice(["JFK", "LGA", "EWR"]) for _ in range(25)], +}) diff --git a/demo/plot_guide_filters_events/run.ipynb b/demo/plot_guide_filters_events/run.ipynb new file mode 100644 index 0000000000..81e74857d9 --- /dev/null +++ b/demo/plot_guide_filters_events/run.ipynb @@ -0,0 +1 @@ +{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: plot_guide_filters_events"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["# Downloading files from the demo repo\n", "import os\n", "!wget -q https://github.com/gradio-app/gradio/raw/main/demo/plot_guide_filters_events/data.py"]}, {"cell_type": "code", "execution_count": null, "id": "44380577570523278879349135829904343037", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "from data import df\n", "\n", "with gr.Blocks() as demo:\n", " with gr.Row():\n", " origin = gr.Dropdown([\"All\", \"DFW\", \"DAL\", \"HOU\"], value=\"All\", label=\"Origin\")\n", " destination = gr.Dropdown([\"All\", \"JFK\", \"LGA\", \"EWR\"], value=\"All\", label=\"Destination\")\n", " max_price = gr.Slider(0, 1000, value=1000, label=\"Max Price\")\n", "\n", " plt = gr.ScatterPlot(df, x=\"time\", y=\"price\", inputs=[origin, destination, max_price])\n", "\n", " @gr.on(inputs=[origin, destination, max_price], outputs=plt)\n", " def filtered_data(origin, destination, max_price):\n", " _df = df[df[\"price\"] <= max_price]\n", " if origin != \"All\":\n", " _df = _df[_df[\"origin\"] == origin]\n", " if destination != \"All\":\n", " _df = _df[_df[\"destination\"] == destination]\n", " return _df\n", "\n", " \n", "if __name__ == \"__main__\":\n", " demo.launch()"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5} \ No newline at end of file diff --git a/demo/plot_guide_filters_events/run.py b/demo/plot_guide_filters_events/run.py new file mode 100644 index 0000000000..96ca666f42 --- /dev/null +++ b/demo/plot_guide_filters_events/run.py @@ -0,0 +1,23 @@ +import gradio as gr +from data import df + +with gr.Blocks() as demo: + with gr.Row(): + origin = gr.Dropdown(["All", "DFW", "DAL", "HOU"], value="All", label="Origin") + destination = gr.Dropdown(["All", "JFK", "LGA", "EWR"], value="All", label="Destination") + max_price = gr.Slider(0, 1000, value=1000, label="Max Price") + + plt = gr.ScatterPlot(df, x="time", y="price", inputs=[origin, destination, max_price]) + + @gr.on(inputs=[origin, destination, max_price], outputs=plt) + def filtered_data(origin, destination, max_price): + _df = df[df["price"] <= max_price] + if origin != "All": + _df = _df[_df["origin"] == origin] + if destination != "All": + _df = _df[_df["destination"] == destination] + return _df + + +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/demo/plot_guide_selection/data.py b/demo/plot_guide_selection/data.py new file mode 100644 index 0000000000..820e047dfa --- /dev/null +++ b/demo/plot_guide_selection/data.py @@ -0,0 +1,10 @@ +import pandas as pd +import numpy as np +import random + +df = pd.DataFrame({ + 'height': np.random.randint(50, 70, 25), + 'weight': np.random.randint(120, 320, 25), + 'age': np.random.randint(18, 65, 25), + 'ethnicity': [random.choice(["white", "black", "asian"]) for _ in range(25)] +}) diff --git a/demo/plot_guide_selection/run.ipynb b/demo/plot_guide_selection/run.ipynb new file mode 100644 index 0000000000..0473fa6b20 --- /dev/null +++ b/demo/plot_guide_selection/run.ipynb @@ -0,0 +1 @@ +{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: plot_guide_selection"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["# Downloading files from the demo repo\n", "import os\n", "!wget -q https://github.com/gradio-app/gradio/raw/main/demo/plot_guide_selection/data.py"]}, {"cell_type": "code", "execution_count": null, "id": "44380577570523278879349135829904343037", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "from data import df\n", "\n", "with gr.Blocks() as demo:\n", " plt = gr.LinePlot(df, x=\"weight\", y=\"height\")\n", " selection_total = gr.Number(label=\"Total Weight of Selection\")\n", "\n", " def select_region(selection: gr.SelectData):\n", " min_w, max_w = selection.index\n", " return df[(df[\"weight\"] >= min_w) & (df[\"weight\"] <= max_w)][\"weight\"].sum()\n", "\n", " plt.select(select_region, None, selection_total)\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5} \ No newline at end of file diff --git a/demo/plot_guide_selection/run.py b/demo/plot_guide_selection/run.py new file mode 100644 index 0000000000..f55638da92 --- /dev/null +++ b/demo/plot_guide_selection/run.py @@ -0,0 +1,15 @@ +import gradio as gr +from data import df + +with gr.Blocks() as demo: + plt = gr.LinePlot(df, x="weight", y="height") + selection_total = gr.Number(label="Total Weight of Selection") + + def select_region(selection: gr.SelectData): + min_w, max_w = selection.index + return df[(df["weight"] >= min_w) & (df["weight"] <= max_w)]["weight"].sum() + + plt.select(select_region, None, selection_total) + +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/demo/plot_guide_zoom/data.py b/demo/plot_guide_zoom/data.py new file mode 100644 index 0000000000..820e047dfa --- /dev/null +++ b/demo/plot_guide_zoom/data.py @@ -0,0 +1,10 @@ +import pandas as pd +import numpy as np +import random + +df = pd.DataFrame({ + 'height': np.random.randint(50, 70, 25), + 'weight': np.random.randint(120, 320, 25), + 'age': np.random.randint(18, 65, 25), + 'ethnicity': [random.choice(["white", "black", "asian"]) for _ in range(25)] +}) diff --git a/demo/plot_guide_zoom/run.ipynb b/demo/plot_guide_zoom/run.ipynb new file mode 100644 index 0000000000..4bcab97abc --- /dev/null +++ b/demo/plot_guide_zoom/run.ipynb @@ -0,0 +1 @@ +{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: plot_guide_zoom"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["# Downloading files from the demo repo\n", "import os\n", "!wget -q https://github.com/gradio-app/gradio/raw/main/demo/plot_guide_zoom/data.py"]}, {"cell_type": "code", "execution_count": null, "id": "44380577570523278879349135829904343037", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "from data import df\n", "\n", "with gr.Blocks() as demo:\n", " plt = gr.LinePlot(df, x=\"weight\", y=\"height\")\n", "\n", " def select_region(selection: gr.SelectData):\n", " min_w, max_w = selection.index\n", " return gr.LinePlot(x_lim=(min_w, max_w)) # type: ignore\n", "\n", " plt.select(select_region, None, plt)\n", " plt.double_click(lambda: gr.LinePlot(x_lim=None), None, plt)\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5} \ No newline at end of file diff --git a/demo/plot_guide_zoom/run.py b/demo/plot_guide_zoom/run.py new file mode 100644 index 0000000000..e5e83ed810 --- /dev/null +++ b/demo/plot_guide_zoom/run.py @@ -0,0 +1,15 @@ +import gradio as gr +from data import df + +with gr.Blocks() as demo: + plt = gr.LinePlot(df, x="weight", y="height") + + def select_region(selection: gr.SelectData): + min_w, max_w = selection.index + return gr.LinePlot(x_lim=(min_w, max_w)) # type: ignore + + plt.select(select_region, None, plt) + plt.double_click(lambda: gr.LinePlot(x_lim=None), None, plt) + +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/demo/plot_guide_zoom_sync/data.py b/demo/plot_guide_zoom_sync/data.py new file mode 100644 index 0000000000..820e047dfa --- /dev/null +++ b/demo/plot_guide_zoom_sync/data.py @@ -0,0 +1,10 @@ +import pandas as pd +import numpy as np +import random + +df = pd.DataFrame({ + 'height': np.random.randint(50, 70, 25), + 'weight': np.random.randint(120, 320, 25), + 'age': np.random.randint(18, 65, 25), + 'ethnicity': [random.choice(["white", "black", "asian"]) for _ in range(25)] +}) diff --git a/demo/plot_guide_zoom_sync/run.ipynb b/demo/plot_guide_zoom_sync/run.ipynb new file mode 100644 index 0000000000..ae4faecc10 --- /dev/null +++ b/demo/plot_guide_zoom_sync/run.ipynb @@ -0,0 +1 @@ +{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: plot_guide_zoom_sync"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["# Downloading files from the demo repo\n", "import os\n", "!wget -q https://github.com/gradio-app/gradio/raw/main/demo/plot_guide_zoom_sync/data.py"]}, {"cell_type": "code", "execution_count": null, "id": "44380577570523278879349135829904343037", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "from data import df\n", "\n", "with gr.Blocks() as demo:\n", " plt1 = gr.LinePlot(df, x=\"weight\", y=\"height\")\n", " plt2 = gr.BarPlot(df, x=\"weight\", y=\"age\", x_bin=10)\n", " plots = [plt1, plt2]\n", "\n", " def select_region(selection: gr.SelectData):\n", " min_w, max_w = selection.index\n", " return [gr.LinePlot(x_lim=(min_w, max_w))] * len(plots) # type: ignore\n", "\n", " for plt in plots:\n", " plt.select(select_region, None, plots)\n", " plt.double_click(lambda: [gr.LinePlot(x_lim=None)] * len(plots), None, plots)\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5} \ No newline at end of file diff --git a/demo/plot_guide_zoom_sync/run.py b/demo/plot_guide_zoom_sync/run.py new file mode 100644 index 0000000000..2687dacf74 --- /dev/null +++ b/demo/plot_guide_zoom_sync/run.py @@ -0,0 +1,18 @@ +import gradio as gr +from data import df + +with gr.Blocks() as demo: + plt1 = gr.LinePlot(df, x="weight", y="height") + plt2 = gr.BarPlot(df, x="weight", y="age", x_bin=10) + plots = [plt1, plt2] + + def select_region(selection: gr.SelectData): + min_w, max_w = selection.index + return [gr.LinePlot(x_lim=(min_w, max_w))] * len(plots) # type: ignore + + for plt in plots: + plt.select(select_region, None, plots) + plt.double_click(lambda: [gr.LinePlot(x_lim=None)] * len(plots), None, plots) + +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/gradio/components/native_plot.py b/gradio/components/native_plot.py index fac1e809a7..8414fd68dc 100644 --- a/gradio/components/native_plot.py +++ b/gradio/components/native_plot.py @@ -38,7 +38,7 @@ class NativePlot(Component): Demos: native_plots """ - EVENTS = [Events.select] + EVENTS = [Events.select, Events.double_click] def __init__( self, diff --git a/gradio/events.py b/gradio/events.py index ddc10fbe29..6411b30338 100644 --- a/gradio/events.py +++ b/gradio/events.py @@ -725,6 +725,9 @@ class Events: doc="This listener is triggered when the user changes the value of the {{ component }}.", ) click = EventListener("click", doc="Triggered when the {{ component }} is clicked.") + double_click = EventListener( + "double_click", doc="Triggered when the {{ component }} is double clicked." + ) submit = EventListener( "submit", doc="This listener is triggered when the user presses the Enter key while the {{ component }} is focused.", diff --git a/gradio/hash_seed.txt b/gradio/hash_seed.txt new file mode 100644 index 0000000000..705dc5d29f --- /dev/null +++ b/gradio/hash_seed.txt @@ -0,0 +1 @@ +f7311342c5e04ba58dd320ef66cfecd0 \ No newline at end of file diff --git a/guides/06_data-science-and-plots/01_creating-plots.md b/guides/06_data-science-and-plots/01_creating-plots.md index 1148d8ef39..76a088b6af 100644 --- a/guides/06_data-science-and-plots/01_creating-plots.md +++ b/guides/06_data-science-and-plots/01_creating-plots.md @@ -45,6 +45,23 @@ If your x-axis is a string type instead, they will act as the category bins auto $code_plot_guide_aggregate_nominal $demo_plot_guide_aggregate_nominal +## Selecting Regions + +You can use the `.select` listener to select regions of a plot. Click and drag on the plot below to select part of the plot. + +$code_plot_guide_selection +$demo_plot_guide_selection + +You can combine this and the `.double_click` listener to create some zoom in/out effects by changing `x_lim` which sets the bounds of the x-axis: + +$code_plot_guide_zoom +$demo_plot_guide_zoom + +If you had multiple plots with the same x column, your event listeners could target the x limits of all other plots so that the x-axes stay in sync. + +$code_plot_guide_zoom_sync +$demo_plot_guide_zoom_sync + ## Making an Interactive Dashboard Take a look how you can have an interactive dashboard where the plots are functions of other Components. diff --git a/guides/06_data-science-and-plots/02_time-plots.md b/guides/06_data-science-and-plots/02_time-plots.md index 1c5c06072d..4396ca4811 100644 --- a/guides/06_data-science-and-plots/02_time-plots.md +++ b/guides/06_data-science-and-plots/02_time-plots.md @@ -18,7 +18,7 @@ $demo_plot_guide_aggregate_temporal ## DateTime Components -You can use `gr.DateTime` to accept input datetime data. This works well with plots for defining the x-axis range for the data. The `x_lim` attribute sets the x-axis bounds. +You can use `gr.DateTime` to accept input datetime data. This works well with plots for defining the x-axis range for the data. $code_plot_guide_datetime $demo_plot_guide_datetime @@ -36,7 +36,7 @@ Try zooming around in the plots and see how DateTimeRange updates. All the plots ## RealTime Data -In many cases, you're working with live, realtime date, not a static dataframe. In this case, you'd the plot to update regularly with a `gr.Timer()`. Assuming there's a `get_data` method that gets the latest dataframe: +In many cases, you're working with live, realtime date, not a static dataframe. In this case, you'd update the plot regularly with a `gr.Timer()`. Assuming there's a `get_data` method that gets the latest dataframe: ```python with gr.Blocks() as demo: @@ -47,7 +47,7 @@ with gr.Blocks() as demo: timer.tick(lambda: [get_data(), get_data()], outputs=[plot1, plot2]) ``` -You can use the `every` shorthand to attach a `Timer` to a component that has a function value: +You can also use the `every` shorthand to attach a `Timer` to a component that has a function value: ```python with gr.Blocks() as demo: diff --git a/guides/06_data-science-and-plots/03_filters-tables-and-stats.md b/guides/06_data-science-and-plots/03_filters-tables-and-stats.md index 1f22e26ed1..dc943dcd79 100644 --- a/guides/06_data-science-and-plots/03_filters-tables-and-stats.md +++ b/guides/06_data-science-and-plots/03_filters-tables-and-stats.md @@ -4,10 +4,14 @@ Your dashboard will likely consist of more than just plots. Let's take a look at ## Filters -Use any of the standard Gradio form components to filter your data. Because the dataframe is not static, we'll use function-as-value format for the LinePlot value. +Use any of the standard Gradio form components to filter your data. You can do this via event listeners or function-as-value syntax. Let's look at the event listener approach first: + +$code_plot_guide_filters_events +$demo_plot_guide_filters_events + +And this would be the function-as-value approach for the same demo. $code_plot_guide_filters -$demo_plot_guide_filters ## Tables and Stats diff --git a/js/nativeplot/Index.svelte b/js/nativeplot/Index.svelte index 2a1bf8b8f4..918bba8a24 100644 --- a/js/nativeplot/Index.svelte +++ b/js/nativeplot/Index.svelte @@ -77,6 +77,7 @@ }[]; export let gradio: Gradio<{ select: SelectData; + double_click: undefined; clear_status: LoadingStatus; }>; @@ -159,6 +160,19 @@ view = result.view; resizeObserver.observe(chart_element); var debounceTimeout: NodeJS.Timeout; + view.addEventListener("dblclick", () => { + gradio.dispatch("double_click"); + }); + // prevent double-clicks from highlighting text + chart_element.addEventListener( + "mousedown", + function (e) { + if (e.detail > 1) { + e.preventDefault(); + } + }, + false + ); if (_selectable) { view.addSignalListener("brush", function (_, value) { if (Object.keys(value).length === 0) return;