Browse Source

Add theme support to ECharts (#4596)

This PR introduces the ability to specify a custom theme with an ECharts
element.

Users can create a JSON theme file using the ECharts Theme Builder here:
https://echarts.apache.org/en/theme-builder.html
The JSON can then be added to a static files path and the URL passed to
the element or a dictionary (matching the format of the JSON theme) can
be passed directly.

Themes are applied to an instance so multiple charts can be displayed on
the same page with different themes.

Example: 
The first chart has a theme passed as a dict, the second is a URL to a
JSON in static files, the third has no theme specified.

![image](https://github.com/user-attachments/assets/56621e39-c014-477a-876e-38d22c18eab2)

---------

Co-authored-by: Falko Schindler <falko@zauberzeug.com>
Simon Robinson 1 tháng trước cách đây
mục cha
commit
6e20ce424b

+ 14 - 1
nicegui/elements/echart.js

@@ -9,7 +9,19 @@ export default {
       await import("echarts-gl");
     }
 
-    this.chart = echarts.init(this.$el, null, { renderer: this.renderer });
+    const theme_name = this.theme ? createRandomUUID() : null;
+    try {
+      if (typeof this.theme == "string") {
+        const response = await fetch(this.theme);
+        echarts.registerTheme(theme_name, await response.json());
+      } else if (this.theme) {
+        echarts.registerTheme(theme_name, this.theme);
+      }
+    } catch (error) {
+      console.error("Could not register theme:", error);
+    }
+
+    this.chart = echarts.init(this.$el, theme_name, { renderer: this.renderer });
     this.chart.on("click", (e) => this.$emit("pointClick", e));
     for (const event of [
       "click",
@@ -96,5 +108,6 @@ export default {
     options: Object,
     enable_3d: Boolean,
     renderer: String,
+    theme: String,
   },
 };

+ 4 - 1
nicegui/elements/echart.py

@@ -1,4 +1,4 @@
-from typing import Callable, Dict, Literal, Optional
+from typing import Callable, Dict, Literal, Optional, Union
 
 from typing_extensions import Self
 
@@ -27,6 +27,7 @@ class EChart(Element,
                  on_point_click: Optional[Handler[EChartPointClickEventArguments]] = None, *,
                  enable_3d: bool = False,
                  renderer: Literal['canvas', 'svg'] = 'canvas',
+                 theme: Optional[Union[str, Dict]] = None,
                  ) -> None:
         """Apache EChart
 
@@ -38,11 +39,13 @@ class EChart(Element,
         :param on_click_point: callback that is invoked when a point is clicked
         :param enable_3d: enforce importing the echarts-gl library
         :param renderer: renderer to use ("canvas" or "svg", *added in version 2.7.0*)
+        :param theme: an EChart theme configuration (dictionary or a URL returning a JSON object, *added in version 2.15.0*)
         """
         super().__init__()
         self._props['options'] = options
         self._props['enable_3d'] = enable_3d or any('3D' in key for key in options)
         self._props['renderer'] = renderer
+        self._props['theme'] = theme
         self._update_method = 'update_chart'
 
         if on_point_click:

+ 37 - 1
tests/test_echart.py

@@ -1,11 +1,21 @@
+from typing import Generator
+
+import pytest
 from pyecharts import options
 from pyecharts.charts import Bar
 from pyecharts.commons import utils
 
-from nicegui import ui
+from nicegui import app, ui
 from nicegui.testing import Screen
 
 
+@pytest.fixture
+def test_route() -> Generator[str, None, None]:
+    TEST_ROUTE = '/theme.json'
+    yield TEST_ROUTE
+    app.remove_route(TEST_ROUTE)
+
+
 def test_create_dynamically(screen: Screen):
     def create():
         ui.echart({
@@ -118,3 +128,29 @@ def test_chart_events(screen: Screen):
 
     screen.open('/')
     screen.should_contain('Chart rendered.')
+
+
+def test_theme_dictionary(screen: Screen):
+    ui.echart({
+        'xAxis': {'type': 'category'},
+        'yAxis': {'type': 'value'},
+        'series': [{'type': 'line', 'data': [1, 2, 3]}],
+    }, theme={'backgroundColor': 'rgba(254,248,239,1)'}, renderer='svg')
+
+    screen.open('/')
+    assert screen.find_by_tag('rect').value_of_css_property('fill') == 'rgb(254, 248, 239)'
+
+
+def test_theme_url(screen: Screen, test_route: str):  # pylint: disable=redefined-outer-name
+    @app.get(test_route)
+    def theme():
+        return {'backgroundColor': 'rgba(254,248,239,1)'}
+
+    ui.echart({
+        'xAxis': {'type': 'category'},
+        'yAxis': {'type': 'value'},
+        'series': [{'type': 'line', 'data': [1, 2, 3]}],
+    }, theme=test_route, renderer='svg')
+
+    screen.open('/')
+    assert screen.find_by_tag('rect').value_of_css_property('fill') == 'rgb(254, 248, 239)'

+ 17 - 0
website/documentation/content/echart_documentation.py

@@ -47,6 +47,23 @@ def dynamic_properties() -> None:
     })
 
 
+@doc.demo('EChart with custom theme', '''
+    You can apply custom themes created with the [Theme Builder](https://echarts.apache.org/en/theme-builder.html).
+
+    Instead of passing the theme as a dictionary, you can pass a URL to a JSON file.
+    This allows the browser to cache the theme and load it faster when the same theme is used multiple times.
+''')
+def custom_theme() -> None:
+    ui.echart({
+        'xAxis': {'type': 'category'},
+        'yAxis': {'type': 'value'},
+        'series': [{'type': 'bar', 'data': [20, 10, 30, 50, 40, 30]}],
+    }, theme={
+        'color': ['#b687ac', '#28738a', '#a78f8f'],
+        'backgroundColor': 'rgba(254,248,239,1)',
+    })
+
+
 @doc.demo('EChart from pyecharts', '''
     You can create an EChart element from a pyecharts object using the `from_pyecharts` method.
     For defining dynamic options like a formatter function, you can use the `JsCode` class from `pyecharts.commons.utils`.