From fc98f7712d910a8a8c0a0d5211af17fa17e24018 Mon Sep 17 00:00:00 2001 From: Marc Skov Madsen Date: Sat, 9 Nov 2024 11:54:02 +0100 Subject: [PATCH] Philipp: Fix main branch [Ready for Review] (#32) --- .github/workflows/pr_checks.yml | 31 + .gitignore | 3 + DEVELOPER_GUIDE.md | 2 +- README.md | 35 +- .../{spec.json => bikesharing_dashboard.json} | 4 +- .../{app.png => bikesharing_dashboard.png} | Bin .../{app.py => bikesharing_dashboard.py} | 14 +- .../bikesharing_dashboard/streamlit_spec.json | 775 ------------------ examples/earthquake_dashboard/app.css | 20 - .../{spec.json => earthquake_dashboard.json} | 0 .../{app.png => earthquake_dashboard.png} | Bin .../{app.py => earthquake_dashboard.py} | 35 +- .../{app.gif => reference_app.gif} | Bin .../{app.py => reference_app.py} | 14 +- pyproject.toml | 9 +- scripts/create_pycafe_links.py | 18 +- src/panel_gwalker/_gwalker.js | 24 +- src/panel_gwalker/_gwalker.py | 85 +- tests/test_graphic_walker.py | 70 +- tests/test_graphic_walker_apps.py | 16 + 20 files changed, 224 insertions(+), 931 deletions(-) create mode 100644 .github/workflows/pr_checks.yml rename examples/bikesharing_dashboard/{spec.json => bikesharing_dashboard.json} (99%) rename examples/bikesharing_dashboard/{app.png => bikesharing_dashboard.png} (100%) rename examples/bikesharing_dashboard/{app.py => bikesharing_dashboard.py} (87%) delete mode 100644 examples/bikesharing_dashboard/streamlit_spec.json delete mode 100644 examples/earthquake_dashboard/app.css rename examples/earthquake_dashboard/{spec.json => earthquake_dashboard.json} (100%) rename examples/earthquake_dashboard/{app.png => earthquake_dashboard.png} (100%) rename examples/earthquake_dashboard/{app.py => earthquake_dashboard.py} (64%) rename examples/reference_app/{app.gif => reference_app.gif} (100%) rename examples/reference_app/{app.py => reference_app.py} (85%) create mode 100644 tests/test_graphic_walker_apps.py diff --git a/.github/workflows/pr_checks.yml b/.github/workflows/pr_checks.yml new file mode 100644 index 0000000..b40360d --- /dev/null +++ b/.github/workflows/pr_checks.yml @@ -0,0 +1,31 @@ +name: PR Checks + +on: + pull_request: + types: [opened, synchronize] + push: + branches: [main] + +jobs: + pr_checks: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + python -m pip install --upgrade uv + uv pip install -e .[dev,tests] + + - name: Run pre-commit + uses: pre-commit/action@v3.0.1 + + - name: Run pytest + run: pytest diff --git a/.gitignore b/.gitignore index f3c2e54..1c519ed 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,9 @@ coverage.xml .pytest_cache/ cover/ +# OSX +.DS_STORE + # Translations *.mo *.pot diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index 1075213..2bf7eb0 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -37,7 +37,7 @@ pytest tests ### Serve the Examples ```bash -panel serve examples/app_*.py --dev +panel serve $(find examples -name "*.py") --dev ``` ### 🚢 Release a new package on Pypi diff --git a/README.md b/README.md index 6c4caae..e09255d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # ✨ Welcome to Panel Graphic Walker [![License](https://img.shields.io/badge/License-MIT%202.0-blue.svg)](https://opensource.org/licenses/MIT) -[![py.cafe](https://py.cafe/badge.svg)](https://py.cafe/snippet/panel/v1#code=https%3A//raw.githubusercontent.com/panel-extensions/panel-graphic-walker/refs/heads/main/examples/examples/reference_app/app.py&requirements=panel-graphic-walker%5Bserver%5D%3E%3D0.4.0) +[![py.cafe](https://py.cafe/badge.svg)](https://py.cafe/snippet/panel/v1#code=https%3A//raw.githubusercontent.com/panel-extensions/panel-graphic-walker/refs/heads/main/examples/reference_app/reference_app.py&requirements=panel-graphic-walker%5Bkernel%5D%3E%3D0.4.0%0Afastparquet) **A simple way to explore your data through a *[Tableau-like](https://www.tableau.com/)* interface directly in your [Panel](https://panel.holoviz.org/) data applications.** @@ -34,7 +34,7 @@ pip install panel-graphic-walker ### Basic Graphic Walker Pane -[![py.cafe](https://py.cafe/badge.svg)](https://py.cafe/snippet/panel/v1#code=https%3A//raw.githubusercontent.com/panel-extensions/panel-graphic-walker/refs/heads/main/examples/examples/reference/basic.py&requirements=panel-graphic-walker%3E%3D0.4.0) [![Static Badge](https://img.shields.io/badge/source-code-blue)](https://github.com/panel-extensions/panel-graphic-walker/blob/main/examples/examples/reference/basic.py) +[![py.cafe](https://py.cafe/badge.svg)](https://py.cafe/snippet/panel/v1#code=https%3A//raw.githubusercontent.com/panel-extensions/panel-graphic-walker/refs/heads/main/examples/reference/basic.py&requirements=panel-graphic-walker%3E%3D0.4.0) [![Static Badge](https://img.shields.io/badge/source-code-blue)](https://github.com/panel-extensions/panel-graphic-walker/blob/main/examples/reference/basic.py) Here’s an example of how to create a simple `GraphicWalker` pane: @@ -57,7 +57,7 @@ You can put the code in a file `app.py` and serve it with `panel serve app.py`. ### Setting the Chart Specification -[![py.cafe](https://py.cafe/badge.svg)](https://py.cafe/snippet/panel/v1#code=https%3A//raw.githubusercontent.com/panel-extensions/panel-graphic-walker/refs/heads/main/examples/examples/reference/spec.py&requirements=panel-graphic-walker%3E%3D0.4.0) [![Static Badge](https://img.shields.io/badge/source-code-blue)](https://github.com/panel-extensions/panel-graphic-walker/blob/main/examples/examples/reference/spec.py) +[![py.cafe](https://py.cafe/badge.svg)](https://py.cafe/snippet/panel/v1#code=https%3A//raw.githubusercontent.com/panel-extensions/panel-graphic-walker/refs/heads/main/examples/reference/spec.py&requirements=panel-graphic-walker%3E%3D0.4.0) [![Static Badge](https://img.shields.io/badge/source-code-blue)](https://github.com/panel-extensions/panel-graphic-walker/blob/main/examples/reference/spec.py) In the `GraphicWalker` UI, you can save your chart specification as a JSON file. You can then open the `GraphicWalker` with the same `spec`: @@ -69,7 +69,7 @@ GraphicWalker(df, spec="spec.json") ### Changing the renderer -[![py.cafe](https://py.cafe/badge.svg)](https://py.cafe/snippet/panel/v1#code=https%3A//raw.githubusercontent.com/panel-extensions/panel-graphic-walker/refs/heads/main/examples/examples/reference/renderer.py&requirements=panel-graphic-walker%3E%3D0.4.0) [![Static Badge](https://img.shields.io/badge/source-code-blue)](https://github.com/panel-extensions/panel-graphic-walker/blob/main/examples/examples/reference/renderer.py) +[![py.cafe](https://py.cafe/badge.svg)](https://py.cafe/snippet/panel/v1#code=https%3A//raw.githubusercontent.com/panel-extensions/panel-graphic-walker/refs/heads/main/examples/reference/renderer.py&requirements=panel-graphic-walker%3E%3D0.4.0) [![Static Badge](https://img.shields.io/badge/source-code-blue)](https://github.com/panel-extensions/panel-graphic-walker/blob/main/examples/reference/renderer.py) You may change the `renderer` to one of 'explorer' (default), 'profiler', 'viewer' or 'chart': @@ -81,7 +81,7 @@ GraphicWalker(df, renderer='profiler') ### Scaling with Server-Side Computation -[![py.cafe](https://py.cafe/badge.svg)](https://py.cafe/snippet/panel/v1#code=https%3A//raw.githubusercontent.com/panel-extensions/panel-graphic-walker/refs/heads/main/examples/examples/reference/kernel_computation.py&requirements=panel-graphic-walker%5Bserver%5D%3E%3D0.4.0) [![Static Badge](https://img.shields.io/badge/source-code-blue)](https://github.com/panel-extensions/panel-graphic-walker/blob/main/examples/examples/reference/kernel_computation.py) +[![py.cafe](https://py.cafe/badge.svg)](https://py.cafe/snippet/panel/v1#code=https%3A//raw.githubusercontent.com/panel-extensions/panel-graphic-walker/refs/heads/main/examples/reference/kernel_computation.py&requirements=panel-graphic-walker%5Bkernel%5D%3E%3D0.4.0%0Afastparquet) [![Static Badge](https://img.shields.io/badge/source-code-blue)](https://github.com/panel-extensions/panel-graphic-walker/blob/main/examples/reference/kernel_computation.py) In some environments, you may encounter message or client-side data limits. To handle larger datasets, you can offload the *computation* to the *server* or Jupyter *kernel*. @@ -103,25 +103,25 @@ Please note that if running on Pyodide, computations will always take place on t ### Explore all the Parameters and Methods -[![py.cafe](https://py.cafe/badge.svg)](https://py.cafe/snippet/panel/v1#code=https%3A//raw.githubusercontent.com/panel-extensions/panel-graphic-walker/refs/heads/main/examples/examples/reference_app/app.py&requirements=panel-graphic-walker%3E%3D0.4.0%0Afastparquet) [![Static Badge](https://img.shields.io/badge/source-code-blue)](https://github.com/panel-extensions/panel-graphic-walker/blob/main/examples/examples/reference_app/app.py) +[![py.cafe](https://py.cafe/badge.svg)](https://py.cafe/snippet/panel/v1#code=https%3A//raw.githubusercontent.com/panel-extensions/panel-graphic-walker/refs/heads/main/examples/reference_app/reference_app.py&requirements=panel-graphic-walker%5Bkernel%5D%3E%3D0.4.0%0Afastparquet) [![Static Badge](https://img.shields.io/badge/source-code-blue)](https://github.com/panel-extensions/panel-graphic-walker/blob/main/examples/reference_app/reference_app.py) To learn more about all the parameters and methods of `GraphicWalker`, try the `panel-graphic-walker` Reference App. -![Panel Graphic Walker Reference App](https://github.com/panel-extensions/panel-graphic-walker/blob/main/examples/reference_app/app.gif) +![Panel Graphic Walker Reference App](https://github.com/panel-extensions/panel-graphic-walker/blob/main/examples/reference_app/reference_app.gif) ## Examples ### Bike Sharing Dashboard -[![py.cafe](https://py.cafe/badge.svg)](https://py.cafe/snippet/panel/v1#code=https%3A//raw.githubusercontent.com/panel-extensions/panel-graphic-walker/refs/heads/main/examples/examples/bikesharing_dashboard/app.py&requirements=panel-graphic-walker%5Bserver%5D%3E%3D0.4.0) [![Static Badge](https://img.shields.io/badge/source-code-blue)](https://github.com/panel-extensions/panel-graphic-walker/blob/main/examples/examples/bikesharing_dashboard/app.py) +[![py.cafe](https://py.cafe/badge.svg)](https://py.cafe/snippet/panel/v1#code=https%3A//raw.githubusercontent.com/panel-extensions/panel-graphic-walker/refs/heads/main/examples/bikesharing_dashboard/bikesharing_dashboard.py&requirements=panel-graphic-walker%5Bkernel%5D%3E%3D0.4.0%0Afastparquet) [![Static Badge](https://img.shields.io/badge/source-code-blue)](https://github.com/panel-extensions/panel-graphic-walker/blob/main/examples/bikesharing_dashboard/bikesharing_dashboard.py) -![Bike Sharing Dashboard](https://github.com/panel-extensions/panel-graphic-walker/blob/main/examples/bikesharing_dashboard/app.png) +![Bike Sharing Dashboard](https://github.com/panel-extensions/panel-graphic-walker/blob/main/examples/bikesharing_dashboard/bikesharing_dashboard.png) ### Earthquake Dashboard -[![py.cafe](https://py.cafe/badge.svg)](https://py.cafe/snippet/panel/v1#code=https%3A//raw.githubusercontent.com/panel-extensions/panel-graphic-walker/refs/heads/main/examples/examples/earthquake_dashboard/app.py&requirements=panel-graphic-walker%5Bserver%5D%3E%3D0.4.0) [![Static Badge](https://img.shields.io/badge/source-code-blue)](https://github.com/panel-extensions/panel-graphic-walker/blob/main/examples/examples/earthquake_dashboard/app.py) +[![py.cafe](https://py.cafe/badge.svg)](https://py.cafe/snippet/panel/v1#code=https%3A//raw.githubusercontent.com/panel-extensions/panel-graphic-walker/refs/heads/main/examples/earthquake_dashboard/earthquake_dashboard.py&requirements=panel-graphic-walker%5Bkernel%5D%3E%3D0.4.0%0Afastparquet) [![Static Badge](https://img.shields.io/badge/source-code-blue)](https://github.com/panel-extensions/panel-graphic-walker/blob/main/examples/earthquake_dashboard/earthquake_dashboard.py) -![Earthquake Dashboard](https://github.com/panel-extensions/panel-graphic-walker/blob/main/examples/earthquake_dashboard/app.png) +![Earthquake Dashboard](https://github.com/panel-extensions/panel-graphic-walker/blob/main/examples/earthquake_dashboard/earthquake_dashboard.png) ## API @@ -130,15 +130,18 @@ To learn more about all the parameters and methods of `GraphicWalker`, try the ` #### Core - `object` (DataFrame): The data for exploration. Please note that if you update the `object`, the existing chart(s) will not be deleted, and you will have to create a new one manually to use the new dataset. -- `fields` (list): Optional specification of fields (columns). +- `field_specs` (list): Optional specification of fields (columns). - `spec` (str, dict, list): Optional chart specification as URL, JSON, dict, or list. Can be generated via the `export` method. - `kernel_computation` (bool): Optional. If True, the computations will take place on the server or in the Jupyter kernel instead of the client to scale to larger datasets. The 'chart' renderer will only work with client side rendering. Default is False. + +#### Renderer + - `renderer` (str): How to display the data. One of 'explorer' (default), 'profiler', 'viewer', or 'chart'. These correspond to `GraphicWalker`, `TableWalker`, `GraphicRenderer`, and `PureRender` in the `graphic-walker` React library. -- `page_size` (int): The number of rows per page in the table. Only applicable for the `profiler` renderer. +- `container_height` (str): The height of a single chart in the `viewer` or `chart` renderer. For example, '500px' (pixels) or '30vh' (viewport height). - `hide_profiling` (bool): Whether to hide the profiling part of the 'profiler' renderer. Default is False. Does not apply to other renderers. - `index` (int | list): Optional index or indices to display. Default is None (all). Only applicable for the `viewer` or `chart` renderer. +- `page_size` (int): The number of rows per page in the table. Only applicable for the `profiler` renderer. - `tab` ('data' | 'vis'): Set the active tab to 'data' or 'vis' (default). Only applicable for the `explorer` renderer. Not bi-directionally synced. -- `container_height` (str): The height of a single chart in the `viewer` or `chart` renderer. For example, '500px' (pixels) or '30vh' (viewport height). #### Style @@ -147,7 +150,7 @@ To learn more about all the parameters and methods of `GraphicWalker`, try the ` #### Other -- `config` (dict): Optional additional configuration for Graphic Walker. See the [Graphic Walker API](https://github.com/Kanaries/graphic-walker#api) for more details. +- `config` (dict): Optional additional configuration for Graphic Walker. For example `{"i18nLang": "ja-JP"}`. See the [Graphic Walker API](https://github.com/Kanaries/graphic-walker#api) for more details. ### Methods @@ -168,7 +171,7 @@ To learn more about all the parameters and methods of `GraphicWalker`, try the ` #### Other Methods -- `calculated_fields`: Returns a list of `fields` calculated from the `object`. This is a great starting point if you want to provide custom `fields`. +- `calculated_field_specs`: Returns a list of *fields* calculated from the `object`. This is a great starting point if you want to provide custom `field_specs`. ## Vision diff --git a/examples/bikesharing_dashboard/spec.json b/examples/bikesharing_dashboard/bikesharing_dashboard.json similarity index 99% rename from examples/bikesharing_dashboard/spec.json rename to examples/bikesharing_dashboard/bikesharing_dashboard.json index 9edff52..23146cf 100644 --- a/examples/bikesharing_dashboard/spec.json +++ b/examples/bikesharing_dashboard/bikesharing_dashboard.json @@ -21,7 +21,7 @@ "fid": "month", "name": "month", "basename": "month", - "semanticType": "ordinal", + "semanticType": "quantitative", "analyticType": "dimension" }, { @@ -307,7 +307,7 @@ "fid": "month", "name": "month", "basename": "month", - "semanticType": "ordinal", + "semanticType": "quantitative", "analyticType": "dimension", "rule": { "type": "range", diff --git a/examples/bikesharing_dashboard/app.png b/examples/bikesharing_dashboard/bikesharing_dashboard.png similarity index 100% rename from examples/bikesharing_dashboard/app.png rename to examples/bikesharing_dashboard/bikesharing_dashboard.png diff --git a/examples/bikesharing_dashboard/app.py b/examples/bikesharing_dashboard/bikesharing_dashboard.py similarity index 87% rename from examples/bikesharing_dashboard/app.py rename to examples/bikesharing_dashboard/bikesharing_dashboard.py index 6b7e1dd..5a77ae8 100644 --- a/examples/bikesharing_dashboard/app.py +++ b/examples/bikesharing_dashboard/bikesharing_dashboard.py @@ -9,8 +9,8 @@ ROOT = Path(__file__).parent # Source: https://kanaries-app.s3.ap-northeast-1.amazonaws.com/public-datasets/bike_sharing_dc.csv -DATASET = "https://datasets.holoviz.org/significant_earthquakes/v1/significant_earthquakes.parquet" -SPEC_PATH = ROOT / "spec.json" +DATASET = "https://datasets.holoviz.org/bikesharing_dc/v1/bikesharing_dc.parquet" +SPEC_PATH = ROOT / "bikesharing_dashboard.json" ACCENT = "#ff4a4a" if pn.config.theme == "dark": @@ -34,19 +34,18 @@ } """ - @pn.cache def get_data(): return pd.read_parquet(DATASET) - data = get_data() walker = GraphicWalker( data, - theme="streamlit", + theme_key="streamlit", spec=SPEC_PATH, sizing_mode="stretch_both", + kernel_computation=True, ) main = pn.Tabs( @@ -85,11 +84,6 @@ def get_data(): This dashboard is built using the **[panel-graphic-walker](https://github.com/panel-extensions/panel-graphic-walker)** \ and inspired by a [similar Streamlit app](https://pygwalkerdemo-cxz7f7pt5oc.streamlit.app/). - -## Notes - -I've simplified the [spec.json](spec.json) file and inserted the `range` filter manually \ -[#654](https://github.com/Kanaries/pygwalker/issues/654). """, ) diff --git a/examples/bikesharing_dashboard/streamlit_spec.json b/examples/bikesharing_dashboard/streamlit_spec.json deleted file mode 100644 index 07685f5..0000000 --- a/examples/bikesharing_dashboard/streamlit_spec.json +++ /dev/null @@ -1,775 +0,0 @@ -{ - "config": [ - { - "config": { - "defaultAggregated": true, - "geoms": [ - "bar" - ], - "coordSystem": "generic", - "limit": -1 - }, - "encodings": { - "dimensions": [ - { - "dragId": "gw_d0SN", - "fid": "date", - "name": "date", - "basename": "date", - "semanticType": "temporal", - "analyticType": "dimension" - }, - { - "dragId": "gw_fCVU", - "fid": "month", - "name": "month", - "basename": "month", - "semanticType": "ordinal", - "analyticType": "dimension" - }, - { - "dragId": "gw_xAWV", - "fid": "season", - "name": "season", - "basename": "season", - "semanticType": "nominal", - "analyticType": "dimension" - }, - { - "dragId": "gw_ho7q", - "fid": "year", - "name": "year", - "basename": "year", - "semanticType": "ordinal", - "analyticType": "dimension" - }, - { - "dragId": "gw_1bIC", - "fid": "holiday", - "name": "holiday", - "basename": "holiday", - "semanticType": "nominal", - "analyticType": "dimension" - }, - { - "dragId": "gw_K8Ek", - "fid": "work yes or not", - "name": "work yes or not", - "basename": "work yes or not", - "semanticType": "ordinal", - "analyticType": "dimension" - }, - { - "dragId": "gw_tORa", - "fid": "am or pm", - "name": "am or pm", - "basename": "am or pm", - "semanticType": "nominal", - "analyticType": "dimension" - }, - { - "dragId": "gw_RMSm", - "fid": "Day of the week", - "name": "Day of the week", - "basename": "Day of the week", - "semanticType": "quantitative", - "analyticType": "dimension" - }, - { - "dragId": "gw_mea_key_fid", - "fid": "gw_mea_key_fid", - "name": "Measure names", - "analyticType": "dimension", - "semanticType": "nominal" - }, - { - "fid": "gw_hYau", - "dragId": "gw_hYau", - "name": "Weekday [date]", - "semanticType": "ordinal", - "analyticType": "dimension", - "aggName": "sum", - "computed": true, - "expression": { - "op": "dateTimeFeature", - "as": "gw_hYau", - "params": [ - { - "type": "field", - "value": "date" - }, - { - "type": "value", - "value": "weekday" - }, - { - "type": "format", - "value": "%Y-%m-%d" - } - ] - } - }, - { - "fid": "gw_lSdd", - "dragId": "gw_lSdd", - "name": "Quarter [date]", - "semanticType": "ordinal", - "analyticType": "dimension", - "aggName": "sum", - "computed": true, - "expression": { - "op": "dateTimeFeature", - "as": "gw_lSdd", - "params": [ - { - "type": "field", - "value": "date" - }, - { - "type": "value", - "value": "quarter" - }, - { - "type": "format", - "value": "%Y-%m-%d" - } - ] - } - } - ], - "measures": [ - { - "dragId": "gw_oE-g", - "fid": "hour", - "name": "hour", - "basename": "hour", - "analyticType": "measure", - "semanticType": "quantitative", - "aggName": "sum" - }, - { - "dragId": "gw_LZNz", - "fid": "temperature", - "name": "temperature", - "basename": "temperature", - "analyticType": "measure", - "semanticType": "quantitative", - "aggName": "sum" - }, - { - "dragId": "gw_JbdF", - "fid": "feeling_temp", - "name": "feeling_temp", - "basename": "feeling_temp", - "analyticType": "measure", - "semanticType": "quantitative", - "aggName": "sum" - }, - { - "dragId": "gw_7hAr", - "fid": "humidity", - "name": "humidity", - "basename": "humidity", - "analyticType": "measure", - "semanticType": "quantitative", - "aggName": "sum" - }, - { - "dragId": "gw_a8mK", - "fid": "winspeed", - "name": "winspeed", - "basename": "winspeed", - "analyticType": "measure", - "semanticType": "quantitative", - "aggName": "sum" - }, - { - "dragId": "gw_Yb-_", - "fid": "casual", - "name": "casual", - "basename": "casual", - "analyticType": "measure", - "semanticType": "quantitative", - "aggName": "sum" - }, - { - "dragId": "gw_fdQ9", - "fid": "registered", - "name": "registered", - "basename": "registered", - "analyticType": "measure", - "semanticType": "quantitative", - "aggName": "sum" - }, - { - "dragId": "gw_Bdj1", - "fid": "count", - "name": "count", - "basename": "count", - "analyticType": "measure", - "semanticType": "quantitative", - "aggName": "sum" - }, - { - "dragId": "gw_count_fid", - "fid": "gw_count_fid", - "name": "Row count", - "analyticType": "measure", - "semanticType": "quantitative", - "aggName": "sum", - "computed": true, - "expression": { - "op": "one", - "params": [], - "as": "gw_count_fid" - } - }, - { - "dragId": "gw_mea_val_fid", - "fid": "gw_mea_val_fid", - "name": "Measure values", - "analyticType": "measure", - "semanticType": "quantitative", - "aggName": "sum" - } - ], - "rows": [ - { - "dragId": "gw_XI5j", - "fid": "registered", - "name": "registered", - "basename": "registered", - "analyticType": "measure", - "semanticType": "quantitative", - "aggName": "sum" - } - ], - "columns": [ - { - "fid": "gw_hYau", - "dragId": "gw_Jx83", - "name": "Weekday [date]", - "semanticType": "ordinal", - "analyticType": "dimension", - "aggName": "sum", - "computed": true, - "expression": { - "op": "dateTimeFeature", - "as": "gw_hYau", - "params": [ - { - "type": "field", - "value": "date" - }, - { - "type": "value", - "value": "weekday" - }, - { - "type": "format", - "value": "%Y-%m-%d" - } - ] - } - } - ], - "color": [ - { - "fid": "gw_lSdd", - "dragId": "gw_BZBe", - "name": "Quarter [date]", - "semanticType": "ordinal", - "analyticType": "dimension", - "aggName": "sum", - "computed": true, - "expression": { - "op": "dateTimeFeature", - "as": "gw_lSdd", - "params": [ - { - "type": "field", - "value": "date" - }, - { - "type": "value", - "value": "quarter" - }, - { - "type": "format", - "value": "%Y-%m-%d" - } - ] - } - } - ], - "opacity": [], - "size": [], - "shape": [], - "radius": [], - "theta": [], - "longitude": [], - "latitude": [], - "geoId": [], - "details": [], - "filters": [ - { - "dragId": "gw_U38p", - "fid": "month", - "name": "month", - "basename": "month", - "semanticType": "ordinal", - "analyticType": "dimension", - "rule": { - "type": "range", - "value": [ - 1, - 12 - ] - } - } - ], - "text": [] - }, - "layout": { - "showActions": false, - "showTableSummary": false, - "stack": "stack", - "interactiveScale": false, - "zeroScale": true, - "size": { - "mode": "fixed", - "width": 350, - "height": 345 - }, - "format": {}, - "geoKey": "name", - "resolve": { - "x": false, - "y": false, - "color": false, - "opacity": false, - "shape": false, - "size": false - }, - "colorPalette": "paired", - "scale": { - "opacity": {}, - "size": {} - }, - "scaleIncludeUnmatchedChoropleth": false, - "useSvg": false - }, - "visId": "gw_YvK3", - "name": "Chart 1" - }, - { - "config": { - "defaultAggregated": true, - "geoms": [ - "auto" - ], - "coordSystem": "generic", - "limit": -1, - "folds": [ - "registered", - "casual" - ] - }, - "encodings": { - "dimensions": [ - { - "dragId": "gw_iwKS", - "fid": "date", - "name": "date", - "basename": "date", - "semanticType": "temporal", - "analyticType": "dimension" - }, - { - "dragId": "gw_qttg", - "fid": "month", - "name": "month", - "basename": "month", - "semanticType": "ordinal", - "analyticType": "dimension" - }, - { - "dragId": "gw_FJZI", - "fid": "season", - "name": "season", - "basename": "season", - "semanticType": "nominal", - "analyticType": "dimension" - }, - { - "dragId": "gw_noqw", - "fid": "year", - "name": "year", - "basename": "year", - "semanticType": "ordinal", - "analyticType": "dimension" - }, - { - "dragId": "gw_S1Op", - "fid": "holiday", - "name": "holiday", - "basename": "holiday", - "semanticType": "nominal", - "analyticType": "dimension" - }, - { - "dragId": "gw_FECQ", - "fid": "work yes or not", - "name": "work yes or not", - "basename": "work yes or not", - "semanticType": "ordinal", - "analyticType": "dimension" - }, - { - "dragId": "gw_F4AV", - "fid": "am or pm", - "name": "am or pm", - "basename": "am or pm", - "semanticType": "nominal", - "analyticType": "dimension" - }, - { - "dragId": "gw_Srun", - "fid": "Day of the week", - "name": "Day of the week", - "basename": "Day of the week", - "semanticType": "quantitative", - "analyticType": "dimension" - }, - { - "dragId": "gw_mea_key_fid", - "fid": "gw_mea_key_fid", - "name": "Measure names", - "analyticType": "dimension", - "semanticType": "nominal" - } - ], - "measures": [ - { - "dragId": "gw_KeT-", - "fid": "hour", - "name": "hour", - "basename": "hour", - "analyticType": "measure", - "semanticType": "quantitative", - "aggName": "sum" - }, - { - "dragId": "gw_rDyp", - "fid": "temperature", - "name": "temperature", - "basename": "temperature", - "analyticType": "measure", - "semanticType": "quantitative", - "aggName": "sum" - }, - { - "dragId": "gw_G71D", - "fid": "feeling_temp", - "name": "feeling_temp", - "basename": "feeling_temp", - "analyticType": "measure", - "semanticType": "quantitative", - "aggName": "sum" - }, - { - "dragId": "gw_Gjrm", - "fid": "humidity", - "name": "humidity", - "basename": "humidity", - "analyticType": "measure", - "semanticType": "quantitative", - "aggName": "sum" - }, - { - "dragId": "gw_2SZj", - "fid": "winspeed", - "name": "winspeed", - "basename": "winspeed", - "analyticType": "measure", - "semanticType": "quantitative", - "aggName": "sum" - }, - { - "dragId": "gw_Pjzq", - "fid": "casual", - "name": "casual", - "basename": "casual", - "analyticType": "measure", - "semanticType": "quantitative", - "aggName": "sum" - }, - { - "dragId": "gw_dBk7", - "fid": "registered", - "name": "registered", - "basename": "registered", - "analyticType": "measure", - "semanticType": "quantitative", - "aggName": "sum" - }, - { - "dragId": "gw_Bju8", - "fid": "count", - "name": "count", - "basename": "count", - "analyticType": "measure", - "semanticType": "quantitative", - "aggName": "sum" - }, - { - "dragId": "gw_count_fid", - "fid": "gw_count_fid", - "name": "Row count", - "analyticType": "measure", - "semanticType": "quantitative", - "aggName": "sum", - "computed": true, - "expression": { - "op": "one", - "params": [], - "as": "gw_count_fid" - } - }, - { - "dragId": "gw_mea_val_fid", - "fid": "gw_mea_val_fid", - "name": "Measure values", - "analyticType": "measure", - "semanticType": "quantitative", - "aggName": "sum" - } - ], - "rows": [ - { - "dragId": "gw_PW8u", - "fid": "gw_mea_val_fid", - "name": "Measure values", - "analyticType": "measure", - "semanticType": "quantitative", - "aggName": "sum" - } - ], - "columns": [ - { - "dragId": "gw_8tGb", - "fid": "date", - "name": "date", - "basename": "date", - "semanticType": "temporal", - "analyticType": "dimension" - } - ], - "color": [ - { - "dragId": "gw_NK9S", - "fid": "gw_mea_key_fid", - "name": "Measure names", - "analyticType": "dimension", - "semanticType": "nominal" - } - ], - "opacity": [], - "size": [], - "shape": [], - "radius": [], - "theta": [], - "longitude": [], - "latitude": [], - "geoId": [], - "details": [], - "filters": [ - { - "dragId": "gw_RBu-", - "fid": "date", - "name": "date", - "basename": "date", - "semanticType": "temporal", - "analyticType": "dimension", - "rule": { - "type": "temporal range", - "value": [ - 1293811200000, - 1356883200000 - ], - "format": "%Y-%m-%d", - "offset": -480 - } - } - ], - "text": [] - }, - "layout": { - "showActions": false, - "showTableSummary": false, - "stack": "stack", - "interactiveScale": false, - "zeroScale": true, - "size": { - "mode": "fixed", - "width": 752, - "height": 360 - }, - "format": {}, - "geoKey": "name", - "resolve": { - "x": false, - "y": false, - "color": false, - "opacity": false, - "shape": false, - "size": false - }, - "scaleIncludeUnmatchedChoropleth": false - }, - "visId": "gw_QwuS", - "name": "Chart 2" - } - ], - "chart_map": {}, - "version": "0.4.5a3", - "workflow_list": [ - { - "workflow": [ - { - "type": "filter", - "filters": [ - { - "fid": "month", - "rule": { - "type": "range", - "value": [ - 1, - 12 - ] - } - } - ] - }, - { - "type": "transform", - "transform": [ - { - "key": "gw_hYau", - "expression": { - "op": "dateTimeFeature", - "as": "gw_hYau", - "params": [ - { - "type": "field", - "value": "date" - }, - { - "type": "value", - "value": "weekday" - }, - { - "type": "format", - "value": "%Y-%m-%d" - }, - { - "type": "displayOffset", - "value": -480 - } - ] - } - }, - { - "key": "gw_lSdd", - "expression": { - "op": "dateTimeFeature", - "as": "gw_lSdd", - "params": [ - { - "type": "field", - "value": "date" - }, - { - "type": "value", - "value": "quarter" - }, - { - "type": "format", - "value": "%Y-%m-%d" - }, - { - "type": "displayOffset", - "value": -480 - } - ] - } - } - ] - }, - { - "type": "view", - "query": [ - { - "op": "aggregate", - "groupBy": [ - "gw_hYau", - "gw_lSdd" - ], - "measures": [ - { - "field": "registered", - "agg": "sum", - "asFieldKey": "registered_sum" - } - ] - } - ] - } - ] - }, - { - "workflow": [ - { - "type": "filter", - "filters": [ - { - "fid": "date", - "rule": { - "type": "temporal range", - "value": [ - 1293811200000, - 1356883200000 - ], - "offset": -480, - "format": "%Y-%m-%d" - } - } - ] - }, - { - "type": "view", - "query": [ - { - "op": "aggregate", - "groupBy": [ - "date" - ], - "measures": [ - { - "field": "registered", - "agg": "sum", - "asFieldKey": "registered_sum" - }, - { - "field": "casual", - "agg": "sum", - "asFieldKey": "casual_sum" - } - ] - } - ] - } - ] - } - ] -} diff --git a/examples/earthquake_dashboard/app.css b/examples/earthquake_dashboard/app.css deleted file mode 100644 index 95b1ba1..0000000 --- a/examples/earthquake_dashboard/app.css +++ /dev/null @@ -1,20 +0,0 @@ -body { - position: relative; - background: none; -} - -body::after { - content: ""; - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-image: url("https://i.ytimg.com/vi/1YLStcrROgw/hq720.jpg"); - background-size: cover; - background-repeat: no-repeat; - background-attachment: fixed; - background-position: center; - opacity: 0.3; /* Adjust transparency level here (0 = fully transparent, 1 = fully opaque) */ - z-index: -1; -} diff --git a/examples/earthquake_dashboard/spec.json b/examples/earthquake_dashboard/earthquake_dashboard.json similarity index 100% rename from examples/earthquake_dashboard/spec.json rename to examples/earthquake_dashboard/earthquake_dashboard.json diff --git a/examples/earthquake_dashboard/app.png b/examples/earthquake_dashboard/earthquake_dashboard.png similarity index 100% rename from examples/earthquake_dashboard/app.png rename to examples/earthquake_dashboard/earthquake_dashboard.png diff --git a/examples/earthquake_dashboard/app.py b/examples/earthquake_dashboard/earthquake_dashboard.py similarity index 64% rename from examples/earthquake_dashboard/app.py rename to examples/earthquake_dashboard/earthquake_dashboard.py index f47a6c9..ada553b 100644 --- a/examples/earthquake_dashboard/app.py +++ b/examples/earthquake_dashboard/earthquake_dashboard.py @@ -6,14 +6,31 @@ from panel_gwalker import GraphicWalker ROOT = Path(__file__).parent -CSS = ROOT / "app.css" +CSS = """ +body { + position: relative; + background: none; +} + +body::after { + content: ""; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-image: url("https://i.ytimg.com/vi/1YLStcrROgw/hq720.jpg"); + background-size: cover; + background-repeat: no-repeat; + background-attachment: fixed; + background-position: center; + opacity: 0.3; /* Adjust transparency level here (0 = fully transparent, 1 = fully opaque) */ + z-index: -1; +} +""" DATASET = "https://datasets.holoviz.org/significant_earthquakes/v1/significant_earthquakes.parquet" -SPEC = ROOT / "spec.json" - - -@pn.cache -def get_css(): - return CSS.read_text() +# https://cdn.jsdelivr.net/gh/panel-extensions/panel-graphic-walker@main/examples/earthquake_dashboard/earthquake_dashboard.json +SPEC = ROOT / "earthquake_dashboard.json" @pn.cache @@ -23,7 +40,7 @@ def get_df() -> pd.DataFrame: return df -pn.extension(raw_css=[get_css()], theme="dark", sizing_mode="stretch_width") +pn.extension(raw_css=[CSS], theme="dark", sizing_mode="stretch_width") df = get_df() @@ -45,7 +62,7 @@ def get_df() -> pd.DataFrame: walker = GraphicWalker( df, kernel_computation=True, - theme="g2", + theme_key="g2", appearance="dark", spec=SPEC, margin=(0, 25, 25, 25), diff --git a/examples/reference_app/app.gif b/examples/reference_app/reference_app.gif similarity index 100% rename from examples/reference_app/app.gif rename to examples/reference_app/reference_app.gif diff --git a/examples/reference_app/app.py b/examples/reference_app/reference_app.py similarity index 85% rename from examples/reference_app/app.py rename to examples/reference_app/reference_app.py index d97ad02..43bf74d 100644 --- a/examples/reference_app/app.py +++ b/examples/reference_app/reference_app.py @@ -6,14 +6,16 @@ from panel_gwalker import GraphicWalker -pn.extension("filedropper", sizing_mode="stretch_width") +pn.extension("filedropper", sizing_mode="stretch_width", notifications=True) ROOT = Path(__file__).parent PANEL_GW_URL = "https://github.com/panel-extensions/panel-graphic-walker" GW_LOGO = "https://kanaries.net/_next/static/media/kanaries-logo.0a9eb041.png" GW_API = "https://github.com/Kanaries/graphic-walker" GW_GUIDE_URL = "https://docs.kanaries.net/graphic-walker/data-viz/create-data-viz" +# https://cdn.jsdelivr.net/gh/panel-extensions/panel-graphic-walker@main/examples/reference_app/spec_simple.json SPEC_CAPACITY_STATE = ROOT / "spec_capacity_state.json" +# https://cdn.jsdelivr.net/gh/panel-extensions/panel-graphic-walker@main/examples/reference_app/spec_capacity_state.json SPEC_SIMPLE = ROOT / "spec_simple.json" ACCENT = "#5B8FF9" @@ -49,7 +51,7 @@ def get_example_download(): get_data(), spec=SPEC_CAPACITY_STATE, sizing_mode="stretch_both", - kernel_computation=True + kernel_computation=True, ) core_settings = pn.Column( walker.param.kernel_computation, @@ -80,7 +82,7 @@ def get_example_download(): _label("Appearance"), pn.widgets.RadioButtonGroup.from_param(walker.param.appearance, **button_style), _label("Theme Key"), - pn.widgets.RadioButtonGroup.from_param(walker.param.theme, **button_style), + pn.widgets.RadioButtonGroup.from_param(walker.param.theme_key, **button_style), name="Style", ) file_upload = pn.widgets.FileDropper( @@ -101,7 +103,7 @@ def get_example_download(): ``` """).format(value=export_controls.param.value) export_section = pn.Column(export_controls, exported, name="Export") -save_section = pn.Column(walker.save_controls("examples/reference_app/spec.json"), name="Save") +save_section = pn.Column(walker.save_controls(), name="Save") docs_section = f"## Docs\n\n- [panel-graphic-walker]({PANEL_GW_URL})\n- [Graphic Walker Usage Guide]({GW_GUIDE_URL})\n- [Graphic Walker API]({GW_API})" @@ -139,6 +141,10 @@ def _update_walker(value): df = pd.read_csv(StringIO(text)) if not df.empty: walker.object = df + # Can be removed once https://github.com/panel-extensions/panel-graphic-walker/issues/33 is resolved + pn.state.notifications.success( + "New dataset uploaded. Add a new chart to use it.", duration=5000 + ) pn.template.FastListTemplate( diff --git a/pyproject.toml b/pyproject.toml index 3a058f6..a4cb92c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,10 +25,11 @@ dev = [ ] tests = [ "duckdb", + "fastparquet", "gw-dsl-parser", "pygwalker", + "pytest-asyncio", "pytest", - "pytest-asyncio" ] examples = [ "duckdb", @@ -37,9 +38,9 @@ examples = [ "pygwalker", ] kernel = [ - "duckdb", - "gw-dsl-parser", - "pygwalker", + "duckdb ; platform_system != 'Emscripten'", + "gw-dsl-parser ; platform_system != 'Emscripten'", + "pygwalker ; platform_system != 'Emscripten'" ] [tool.hatch.build.targets.wheel] diff --git a/scripts/create_pycafe_links.py b/scripts/create_pycafe_links.py index 5d3e98b..f7ba958 100644 --- a/scripts/create_pycafe_links.py +++ b/scripts/create_pycafe_links.py @@ -12,17 +12,17 @@ BASE_REQUIREMENTS = ["panel-graphic-walker>=0.4.0"] PARQUET_REQUIREMENTS = BASE_REQUIREMENTS + ["fastparquet"] -SERVER_REQUIREMENTS = ["panel-graphic-walker[kernel]>=0.4.0"] +SERVER_REQUIREMENTS = ["panel-graphic-walker[kernel]>=0.4.0", "fastparquet"] EXAMPLES = [ - ("examples/reference/basic.py", BASE_REQUIREMENTS), - ("examples/reference/spec.py", BASE_REQUIREMENTS), - ("examples/reference/renderer.py", BASE_REQUIREMENTS), - ("examples/reference/kernel_computation.py", SERVER_REQUIREMENTS), - ("examples/reference_app/app.py", SERVER_REQUIREMENTS), - ("examples/bikesharing_dashboard/app.py", SERVER_REQUIREMENTS), - ("examples/earthquake_dashboard/app.py", SERVER_REQUIREMENTS), + ("reference/basic.py", BASE_REQUIREMENTS), + ("reference/spec.py", BASE_REQUIREMENTS), + ("reference/renderer.py", BASE_REQUIREMENTS), + ("reference/kernel_computation.py", SERVER_REQUIREMENTS), + ("reference_app/reference_app.py", SERVER_REQUIREMENTS), + ("bikesharing_dashboard/bikesharing_dashboard.py", SERVER_REQUIREMENTS), + ("earthquake_dashboard/earthquake_dashboard.py", SERVER_REQUIREMENTS), ] @@ -51,7 +51,7 @@ def create_example(file, pycafe_url: str, source_code_url: str): def check_file(file): - path = Path(file) + path = Path("examples") / file if not path.exists(): raise FileNotFoundError(f"File {file} does not exist.") diff --git a/src/panel_gwalker/_gwalker.js b/src/panel_gwalker/_gwalker.js index 5e2a67d..f17af03 100644 --- a/src/panel_gwalker/_gwalker.js +++ b/src/panel_gwalker/_gwalker.js @@ -30,7 +30,7 @@ function fetchSpec(url) { }); } -function transformSpec(spec, fields) { +function transformSpec(spec) { /* The spec must be an null or array of objects */ if (spec === null) { return null; @@ -52,10 +52,10 @@ function transformSpec(spec, fields) { export function render({ model }) { // Model state const [appearance] = model.useState('appearance') - const [themeKey] = model.useState('theme') + const [themeKey] = model.useState('theme_key') const [config] = model.useState('config') const [data] = model.useState('object') - const [fields] = model.useState('fields') + const [fields] = model.useState('field_specs') const [spec] = model.useState('spec') const [kernelComputation] = model.useState('kernel_computation') const [renderer] = model.useState('renderer') @@ -185,7 +185,14 @@ export function render({ model }) { }) }, [containerHeight]) - if (renderer=='profiler') { + useEffect(() => { + if (storeRef.current === null) { + return + } + storeRef.current.resetVisualization() + }, [fields]) + + if (renderer === "profiler") { return - } - - if (renderer=='viewer') { + } else if (renderer === "viewer") { // See https://github.com/Kanaries/pygwalker/blob/main/app/src/index.tsx#L466 - return ( <> {transformedIndexSpec?.map((chart, index) => ( @@ -227,9 +231,7 @@ export function render({ model }) { ))} ); - } - - if (renderer=='chart') { + } else if (renderer === "chart") { if (!data | !transformedData) { return
No data to render. Set 'kernel_computation=False' when creating GraphicWalker.
; } diff --git a/src/panel_gwalker/_gwalker.py b/src/panel_gwalker/_gwalker.py index bcbe334..06d1e31 100644 --- a/src/panel_gwalker/_gwalker.py +++ b/src/panel_gwalker/_gwalker.py @@ -6,7 +6,16 @@ from functools import partial from os import PathLike from pathlib import Path -from typing import IO, Any, Callable, Concatenate, Coroutine, Literal, Optional, ParamSpec +from typing import ( + IO, + Any, + Callable, + Concatenate, + Coroutine, + Literal, + Optional, + ParamSpec, +) import numpy as np import pandas as pd @@ -17,9 +26,7 @@ from panel.layout import Column from panel.pane import Markdown from panel.viewable import Viewer -from panel.widgets import ( - Button, IntInput, RadioButtonGroup, TextInput -) +from panel.widgets import Button, IntInput, RadioButtonGroup, TextInput from panel_gwalker._pygwalker import get_data_parser, get_sql_from_payload from panel_gwalker._utils import ( @@ -114,10 +121,7 @@ def __init__( button_type="primary", **layout_params, ), - IntInput.from_param( - self.param.timeout, - **layout_params - ) + IntInput.from_param(self.param.timeout, **layout_params), ) # Should be changed to IconButton once https://github.com/holoviz/panel/issues/7458 is fixed. button = Button.from_param( @@ -149,9 +153,9 @@ def __panel__(self): class SaveControls(ExportControls): """ - A UI component to save the Chart(s) spec of SVG(s). + A UI component to save the Chart(s) spec or SVG(s). - Will save to `save_path` path of the `walker`. + Will save to the `save_path` path. """ save_path: str | PathLike = param.ClassSelector( @@ -180,13 +184,12 @@ def __init__( name=name, description=description, include_settings=include_settings, - **dict(params, **layout_params) + **dict(params, **layout_params), ) if include_settings: - save_path = TextInput.from_param( - self.param.save_path, **layout_params - ) - self._layout.insert(3, save_path) + if isinstance(self.save_path, str): + save_path = TextInput.from_param(self.param.save_path, **layout_params) + self._layout.insert(3, save_path) @param.depends("run", watch=True) async def _export(self): @@ -221,17 +224,26 @@ class GraphicWalker(ReactComponent): # Display the interactive graphic interface GraphicWalker(df).servable() ``` + + If the `GraphicWalker` does not display you may have hit a limit and need to enable the + `kernel_computation`: + + ```python + GraphicWalker(df, kernel_computation=True).servable() + ``` """ object: pd.DataFrame = param.DataFrame( doc="""The data to explore. Please note that if you update the `object`, then the existing charts will not be deleted.""" ) - fields: list = param.List(doc="""Optional fields, i.e. columns, specification.""") + field_specs: list = param.List( + doc="""Optional fields, i.e. columns, specification.""" + ) # Can be replaced with ClassSelector once https://github.com/holoviz/panel/pull/7454 is released spec: SpecType = Spec( doc="""Optional chart specification as url, json, dict or list. - Can be generated via the `export` method.""" + Can be generated via the `export_chart` method.""" ) kernel_computation: bool = param.Boolean( default=False, @@ -280,7 +292,7 @@ class GraphicWalker(ReactComponent): doc="""Dark mode preference: 'light', 'dark' or 'media'. If not provided the appearance is derived from pn.config.theme.""", ) - theme: Literal["g2", "streamlit", "vega"] = param.Selector( + theme_key: Literal["g2", "streamlit", "vega"] = param.Selector( default="g2", objects=["g2", "streamlit", "vega"], doc="""The theme of the chart(s). One of 'g2', 'streamlit' or 'vega' (default).""", @@ -330,7 +342,7 @@ def _get_appearance(self, theme): return config.get(theme, self.param.appearance.default) @param.depends("object") - def calculated_fields(self) -> list[dict]: + def calculated_field_specs(self) -> list[dict]: """Returns all the fields calculated from the object. The calculated fields are a great starting point if you want to customize the fields. @@ -339,8 +351,8 @@ def calculated_fields(self) -> list[dict]: def _process_param_change(self, params): if params.get("object") is not None: - if not self.fields: - params["fields"] = self.calculated_fields() + if not self.field_specs: + params["field_specs"] = self.calculated_field_specs() if not self.config: params["config"] = {} if self.kernel_computation: @@ -353,7 +365,7 @@ def _process_param_change(self, params): def _compute(self, payload): logger.debug("request: %s", payload) - field_specs = self.fields or self.calculated_fields() + field_specs = self.field_specs or self.calculated_field_specs() parser = get_data_parser( self.object, field_specs=field_specs, @@ -370,6 +382,7 @@ def _compute(self, payload): {"pygwalker_mid_table": parser.field_metas}, ) logger.exception("SQL raised exception:\n%s\n\npayload:%s", sql, payload) + result = pd.DataFrame() df = pd.DataFrame.from_records(result) logger.debug("response:\n%s", df) @@ -381,18 +394,16 @@ def _handle_msg(self, msg: Any) -> None: if action == "export" and event_id in self._exports: self._exports[event_id] = msg["data"] elif action == "compute": - self._send_msg( - { - "action": "compute", - "id": event_id, - "result": self._compute(msg["payload"]), - } - ) + self._send_msg({ + "action": "compute", + "id": event_id, + "result": self._compute(msg["payload"]), + }) async def export_chart( self, - mode: Literal["spec", "svg"] = 'spec', - scope: Literal["current", "all"] = 'current', + mode: Literal["spec", "svg"] = "spec", + scope: Literal["current", "all"] = "current", timeout: int = 5000, ): """ @@ -465,14 +476,18 @@ def export_controls(self, **params) -> SaveControls: """ return ExportControls(self, **params) - def save_controls(self, save_path: str | os.PathLike | IO, **params) -> SaveControls: + def save_controls( + self, + save_path: str | os.PathLike | IO = SaveControls.param.save_path.default, + **params, + ) -> SaveControls: """Returns a UI component to save the chart(s) as either a spec or SVG. - >>> walker.create_save_button(width=400) + >>> walker.save_controls(width=400) - The spec or SVG will be saved to the path give by `save_path`. + The spec or SVG will be saved to the path given by `save_path`. """ - return SaveControls(self, **params) + return SaveControls(self, save_path=save_path, **params) def chart(self, index: int | list | None = None, **params) -> "GraphicWalker": """Returns a clone with `renderer='chart'` and `kernel_computation=False`. diff --git a/tests/test_graphic_walker.py b/tests/test_graphic_walker.py index 8c7180b..4bb6506 100644 --- a/tests/test_graphic_walker.py +++ b/tests/test_graphic_walker.py @@ -1,6 +1,7 @@ import json from asyncio import sleep from pathlib import Path +from unittest.mock import patch import pandas as pd import param @@ -23,7 +24,7 @@ def default_appearance(): def _get_params(gwalker): return { "object": gwalker.object, - "fields": gwalker.fields, + "field_specs": gwalker.field_specs, "appearance": gwalker.appearance, "config": gwalker.config, "spec": gwalker.spec, @@ -34,7 +35,7 @@ def _get_params(gwalker): def test_constructor(data, default_appearance): gwalker = GraphicWalker(object=data) assert gwalker.object is data - assert not gwalker.fields + assert not gwalker.field_specs assert not gwalker.config assert gwalker.appearance == default_appearance assert gwalker.theme_key == "g2" @@ -45,13 +46,13 @@ def test_process_parameter_change(data, default_appearance): params = _get_params(gwalker) gwalker._process_param_change(params) - assert params["fields"] == gwalker.calculated_fields() + assert params["field_specs"] == gwalker.calculated_field_specs() assert params["appearance"] == default_appearance assert not params["config"] def test_process_parameter_change_with_fields(data, default_appearance): - fields = fields = [ + field_specs = [ { "fid": "t_county", "name": "t_county", @@ -59,11 +60,11 @@ def test_process_parameter_change_with_fields(data, default_appearance): "analyticType": "dimension", }, ] - gwalker = GraphicWalker(object=data, fields=fields) + gwalker = GraphicWalker(object=data, field_specs=field_specs) params = _get_params(gwalker) gwalker._process_param_change(params) - assert params["fields"] is fields + assert params["field_specs"] is field_specs assert params["appearance"] == default_appearance assert not params["config"] @@ -74,7 +75,7 @@ def test_process_parameter_change_with_config(data, default_appearance): params = _get_params(gwalker) gwalker._process_param_change(params) - assert params["fields"] + assert params["field_specs"] assert params["appearance"] == default_appearance assert params["config"] is config @@ -111,7 +112,7 @@ def test_kernel_computation(data): def test_calculated_fields(data): gwalker = GraphicWalker(object=data) - assert gwalker.calculated_fields() == _raw_fields(data) + assert gwalker.calculated_field_specs() == _raw_fields(data) def test_process_spec(data, tmp_path: Path): @@ -158,57 +159,56 @@ def _process_spec(spec): assert result == json_data, f"Expected JSON content from file, got {result}" -async def _mock_export(*args, **kwargs): +async def _mock_export(self, *args, **kwargs): return {"args": args, "kwargs": kwargs} def test_can_create_export_settings(data): - gwalker = GraphicWalker(object=data, export=_mock_export) - assert gwalker.create_export_settings(width=400) + gwalker = GraphicWalker(object=data) + assert gwalker.export_controls(width=400) @pytest.mark.asyncio async def test_export(data): - gwalker = GraphicWalker(object=data, export=_mock_export) - assert isinstance(gwalker.param.export, param.Action) - assert await gwalker.export() + with patch.object(GraphicWalker, "export_chart", _mock_export): + gwalker = GraphicWalker(object=data) + assert await gwalker.export_chart() @pytest.mark.asyncio async def test_export_button(data): - gwalker = GraphicWalker(object=data, export=_mock_export) - button = gwalker.create_export_button(width=400) - assert not button.value - button.param.trigger("export") - await sleep(0.01) - assert button.value + with patch.object(GraphicWalker, "export_chart", _mock_export): + gwalker = GraphicWalker(object=data) + button = gwalker.export_controls(width=400) + assert not button.value + button.param.trigger("run") + await sleep(0.01) + assert button.value @pytest.mark.asyncio async def test_can_save(data, tmp_path, export=_mock_export): - gwalker = GraphicWalker(object=data) - assert isinstance(gwalker.param.save, param.Action) - - gwalker._export = _mock_export # type: ignore[method-assign] - path = tmp_path / "spec.json" - await gwalker.save(path=path) - assert path.exists() + with patch.object(GraphicWalker, "export_chart", _mock_export): + gwalker = GraphicWalker(object=data) + path = tmp_path / "spec.json" + await gwalker.save_chart(path=path) + assert path.exists() @pytest.mark.asyncio async def test_save_button(data, tmp_path: Path): - gwalker = GraphicWalker(object=data, export=_mock_export) - gwalker._export = _mock_export # type: ignore[method-assign] - gwalker.save_path = tmp_path / "spec.json" + with patch.object(GraphicWalker, "export_chart", _mock_export): + gwalker = GraphicWalker(object=data) - button = gwalker.create_save_button(width=400) - button.param.trigger("save") - await sleep(0.1) - assert gwalker.save_path.exists() + save_path = tmp_path / "spec.json" + button = gwalker.save_controls(save_path=save_path, width=400) + button.param.trigger("run") + await sleep(0.1) + assert save_path.exists() def test_page_size(data): - gwalker = GraphicWalker(object=data, export=_mock_export, page_size=50) + gwalker = GraphicWalker(object=data, page_size=50) assert gwalker.page_size == 50 diff --git a/tests/test_graphic_walker_apps.py b/tests/test_graphic_walker_apps.py new file mode 100644 index 0000000..1efd327 --- /dev/null +++ b/tests/test_graphic_walker_apps.py @@ -0,0 +1,16 @@ +from pathlib import Path + +import pytest + +EXAMPLE_APP_PATHS = [str(path) for path in Path("examples").rglob("*.py")] + + +@pytest.mark.parametrize("path", EXAMPLE_APP_PATHS) +def test_apps(path): + # Quick test that apps can run + # Could be improved but I would like to keep theme relatively fast, i.e. without Playwrigth + with open(path) as f: + code = f.read() + env = globals().copy() + env["__file__"] = path + exec(code, env)