Ver código fonte

Merge branch 'main' into markdown_speedup

Rodja Trappe 2 anos atrás
pai
commit
65f706e310

+ 2 - 2
README.md

@@ -41,7 +41,7 @@ NiceGUI is available as [PyPI package](https://pypi.org/project/nicegui/), [Dock
 - notifications, dialogs and menus to provide state of the art user interaction
 - notifications, dialogs and menus to provide state of the art user interaction
 - shared and individual web pages
 - shared and individual web pages
 - ability to add custom routes and data responses
 - ability to add custom routes and data responses
-- capture keyboard input for global shortcuts etc
+- capture keyboard input for global shortcuts etc.
 - customize look by defining primary, secondary and accent colors
 - customize look by defining primary, secondary and accent colors
 - live-cycle events and session data
 - live-cycle events and session data
 
 
@@ -82,7 +82,7 @@ You may also have a look at [our in-depth demonstrations](https://github.com/zau
 
 
 ## Why?
 ## Why?
 
 
-We, at [Zauberzeug](https://zauberzeug.com), like [Streamlit](https://streamlit.io/)
+We at [Zauberzeug](https://zauberzeug.com) like [Streamlit](https://streamlit.io/)
 but find it does [too much magic](https://github.com/zauberzeug/nicegui/issues/1#issuecomment-847413651) when it comes to state handling.
 but find it does [too much magic](https://github.com/zauberzeug/nicegui/issues/1#issuecomment-847413651) when it comes to state handling.
 In search for an alternative nice library to write simple graphical user interfaces in Python we discovered [JustPy](https://justpy.io/).
 In search for an alternative nice library to write simple graphical user interfaces in Python we discovered [JustPy](https://justpy.io/).
 Although we liked the approach, it is too "low-level HTML" for our daily usage.
 Although we liked the approach, it is too "low-level HTML" for our daily usage.

+ 3 - 3
examples/opencv_webcam/main.py

@@ -11,13 +11,13 @@ from nicegui import app, ui
 black_1px = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUGFdjYGBg+A8AAQQBAHAgZQsAAAAASUVORK5CYII='
 black_1px = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUGFdjYGBg+A8AAQQBAHAgZQsAAAAASUVORK5CYII='
 placeholder = Response(content=base64.b64decode(black_1px.encode('ascii')), media_type='image/png')
 placeholder = Response(content=base64.b64decode(black_1px.encode('ascii')), media_type='image/png')
 
 
-# openCV is used to accesss the webcam
+# OpenCV is used to access the webcam
 video_capture = cv2.VideoCapture(0)
 video_capture = cv2.VideoCapture(0)
 
 
 
 
 @app.get('/video/frame')
 @app.get('/video/frame')
 async def grab_video_frame() -> Response:
 async def grab_video_frame() -> Response:
-    # thanks to FastAPI it's easy to create a web route which always provides the latest image from openCV
+    # thanks to FastAPI it is easy to create a web route which always provides the latest image from OpenCV
     if not video_capture.isOpened():
     if not video_capture.isOpened():
         return placeholder
         return placeholder
     ret, frame = video_capture.read()
     ret, frame = video_capture.read()
@@ -27,7 +27,7 @@ async def grab_video_frame() -> Response:
     jpeg = imencode_image.tobytes()
     jpeg = imencode_image.tobytes()
     return Response(content=jpeg, media_type='image/jpeg')
     return Response(content=jpeg, media_type='image/jpeg')
 
 
-# For non-flickering image updates an interactive image is much better than ui.image()
+# For non-flickering image updates an interactive image is much better than ui.image().
 video_image = ui.interactive_image().classes('w-full h-full')
 video_image = ui.interactive_image().classes('w-full h-full')
 # A timer constantly updates the source of the image.
 # A timer constantly updates the source of the image.
 # But because the path is always the same, we must force an update by adding the current timestamp to the source.
 # But because the path is always the same, we must force an update by adding the current timestamp to the source.

+ 47 - 0
examples/svg_clock/main.py

@@ -0,0 +1,47 @@
+#!/usr/bin/env python3
+from datetime import datetime
+
+from nicegui import ui
+
+
+def build_svg() -> str:
+    '''Returns an SVG showing the current time.
+    Original was borrowed from https://de.m.wikipedia.org/wiki/Datei:Station_Clock.svg.'''
+    now = datetime.now()
+    return f'''
+<svg width="800" height="800" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+	<circle cx="400" cy="400" r="400" fill="#fff"/>
+	<use transform="matrix(-1,0,0,1,800,0)" xlink:href="#c"/>
+	<g id="c">
+		<g id="d">
+			<path d="m400 40v107" stroke="#000" stroke-width="26.7"/>
+			<g id="a">
+				<path d="m580 88.233-42.5 73.612" stroke="#000" stroke-width="26.7"/>
+				<g id="e">
+					<path id="b" d="m437.63 41.974-3.6585 34.808" stroke="#000" stroke-width="13.6"/>
+					<use transform="rotate(6 400 400)" xlink:href="#b"/>
+				</g>
+				<use transform="rotate(12 400 400)" xlink:href="#e"/>
+			</g>
+			<use transform="rotate(30 400 400)" xlink:href="#a"/>
+			<use transform="rotate(60 400 400)" xlink:href="#a"/>
+		</g>
+		<use transform="rotate(90 400 400)" xlink:href="#d"/>
+	</g>
+    <g transform="rotate({250 + now.hour / 12 * 360} 400 400)">
+    	<path d="m334.31 357.65-12.068 33.669 283.94 100.8 23.565-10.394-13.332-24.325z"/>
+    </g>
+    <g transform="rotate({117 + now.minute / 60 * 360} 400 400)">
+    	<path d="m480.73 344.98 11.019 21.459-382.37 199.37-18.243-7.2122 4.768-19.029z"/>
+    </g>
+    <g transform="rotate({169 + now.second / 60 * 360} 400 400)">
+        <path d="m410.21 301.98-43.314 242.68a41.963 41.963 0 0 0-2.8605-0.091 41.963 41.963 0 0 0-41.865 42.059 41.963 41.963 0 0 0 30.073 40.144l-18.417 103.18 1.9709 3.9629 3.2997-2.9496 21.156-102.65a41.963 41.963 0 0 0 3.9771 0.1799 41.963 41.963 0 0 0 41.865-42.059 41.963 41.963 0 0 0-29.003-39.815l49.762-241.44zm-42.448 265.56a19.336 19.336 0 0 1 15.703 18.948 19.336 19.336 0 0 1-19.291 19.38 19.336 19.336 0 0 1-19.38-19.291 19.336 19.336 0 0 1 19.291-19.38 19.336 19.336 0 0 1 3.6752 0.3426z" fill="#a40000"/>
+    </g>
+</svg>
+'''
+
+
+clock = ui.html().classes('self-center')
+ui.timer(1, lambda: clock.set_content(build_svg()))
+
+ui.run()

+ 1 - 0
main.py

@@ -206,6 +206,7 @@ ui.run()
             example_link('Image Mask Overlay', 'shows how to overlay an image with a mask')
             example_link('Image Mask Overlay', 'shows how to overlay an image with a mask')
             example_link('Infinite Scroll', 'presents an infinitely scrolling image gallery')
             example_link('Infinite Scroll', 'presents an infinitely scrolling image gallery')
             example_link('OpenCV Webcam', 'uses OpenCV to capture images from a webcam')
             example_link('OpenCV Webcam', 'uses OpenCV to capture images from a webcam')
+            example_link('SVG Clock', 'displays an analog clock by updating an SVG with `ui.timer`')
 
 
     with ui.row().classes('bg-primary w-full min-h-screen mt-16'):
     with ui.row().classes('bg-primary w-full min-h-screen mt-16'):
         link_target('why')
         link_target('why')

+ 2 - 0
nicegui/elements/chart.js

@@ -8,6 +8,8 @@ export default {
   },
   },
   methods: {
   methods: {
     update_chart() {
     update_chart() {
+      while (this.chart.series.length > this.options.series.length) this.chart.series[0].remove();
+      while (this.chart.series.length < this.options.series.length) this.chart.addSeries({}, false);
       this.chart.update(this.options);
       this.chart.update(this.options);
     },
     },
   },
   },

+ 5 - 4
nicegui/elements/interactive_image.py

@@ -13,22 +13,23 @@ register_component('interactive_image', __file__, 'interactive_image.js')
 class InteractiveImage(SourceElement, ContentElement):
 class InteractiveImage(SourceElement, ContentElement):
 
 
     def __init__(self, source: str = '', *,
     def __init__(self, source: str = '', *,
+                 content: str = '',
                  on_mouse: Optional[Callable] = None, events: List[str] = ['click'], cross: bool = False) -> None:
                  on_mouse: Optional[Callable] = None, events: List[str] = ['click'], cross: bool = False) -> None:
         """Interactive Image
         """Interactive Image
 
 
         Create an image with an SVG overlay that handles mouse events and yields image coordinates.
         Create an image with an SVG overlay that handles mouse events and yields image coordinates.
-        It's also the best choice for non-flickering image updates.
-        If the url changes of source faster than images can be loaded by the browser, some images are simply skipped.
+        It is also the best choice for non-flickering image updates.
+        If the source URL changes faster than images can be loaded by the browser, some images are simply skipped.
         Thereby a stream of images automatically adapts to the available bandwidth.
         Thereby a stream of images automatically adapts to the available bandwidth.
         See `OpenCV Webcam <https://github.com/zauberzeug/nicegui/tree/main/examples/opencv_webcam/main.py>`_ for an example.
         See `OpenCV Webcam <https://github.com/zauberzeug/nicegui/tree/main/examples/opencv_webcam/main.py>`_ for an example.
 
 
         :param source: the source of the image; can be an URL or a base64 string
         :param source: the source of the image; can be an URL or a base64 string
-        :param content: svg content which should be overlayed; viewport has the same dimensions as the image
+        :param content: SVG content which should be overlayed; viewport has the same dimensions as the image
         :param on_mouse: callback for mouse events (yields `type`, `image_x` and `image_y`)
         :param on_mouse: callback for mouse events (yields `type`, `image_x` and `image_y`)
         :param events: list of JavaScript events to subscribe to (default: `['click']`)
         :param events: list of JavaScript events to subscribe to (default: `['click']`)
         :param cross: whether to show crosshairs (default: `False`)
         :param cross: whether to show crosshairs (default: `False`)
         """
         """
-        super().__init__(tag='interactive_image', source=source, content='')
+        super().__init__(tag='interactive_image', source=source, content=content)
         self._props['events'] = events
         self._props['events'] = events
         self._props['cross'] = cross
         self._props['cross'] = cross
 
 

+ 1 - 1
nicegui/elements/link.py

@@ -20,7 +20,7 @@ class Link(TextElement):
         """
         """
         super().__init__(tag='a', text=text)
         super().__init__(tag='a', text=text)
         self._props['href'] = target if isinstance(target, str) else globals.page_routes[target]
         self._props['href'] = target if isinstance(target, str) else globals.page_routes[target]
-        self._classes.extend(['underline, text-blue'])
+        self._classes.extend(['underline', 'text-blue'])
 
 
 
 
 class LinkTarget(Element):
 class LinkTarget(Element):

+ 0 - 1
nicegui/elements/mixins/content_element.py

@@ -26,7 +26,6 @@ class ContentElement(Element):
         return self
         return self
 
 
     def set_content(self, content: str) -> None:
     def set_content(self, content: str) -> None:
-        '''changes the content'''
         self.content = content
         self.content = content
 
 
     def on_content_change(self, content: str) -> None:
     def on_content_change(self, content: str) -> None:

+ 0 - 1
nicegui/elements/mixins/source_element.py

@@ -26,7 +26,6 @@ class SourceElement(Element):
         return self
         return self
 
 
     def set_source(self, source: str) -> None:
     def set_source(self, source: str) -> None:
-        '''changes the image source'''
         self.source = source
         self.source = source
 
 
     def on_source_change(self, source: str) -> None:
     def on_source_change(self, source: str) -> None:

+ 6 - 1
tests/screen.py

@@ -93,7 +93,12 @@ class Screen:
     def find(self, text: str) -> WebElement:
     def find(self, text: str) -> WebElement:
         try:
         try:
             query = f'//*[not(self::script) and not(self::style) and contains(text(), "{text}")]'
             query = f'//*[not(self::script) and not(self::style) and contains(text(), "{text}")]'
-            return self.selenium.find_element(By.XPATH, query)
+            element = self.selenium.find_element(By.XPATH, query)
+            if not element.is_displayed():
+                self.wait(0.1)  # HACK: repeat check after a short delay to avoid timing issue on fast machines
+                if not element.is_displayed():
+                    raise AssertionError(f'Found "{text}" but it is hidden')
+            return element
         except NoSuchElementException:
         except NoSuchElementException:
             raise AssertionError(f'Could not find "{text}"')
             raise AssertionError(f'Could not find "{text}"')
 
 

+ 73 - 0
tests/test_chart.py

@@ -0,0 +1,73 @@
+from selenium.webdriver.common.by import By
+
+from nicegui import ui
+
+from .screen import Screen
+
+
+def test_change_chart_series(screen: Screen):
+    chart = ui.chart({
+        'chart': {'type': 'bar'},
+        'xAxis': {'categories': ['A', 'B']},
+        'series': [
+            {'name': 'Alpha', 'data': [0.1, 0.2]},
+            {'name': 'Beta', 'data': [0.3, 0.4]},
+        ],
+    }).classes('w-full h-64')
+
+    def update():
+        chart.options['series'][0]['data'][:] = [1, 1]
+        chart.update()
+
+    ui.button('Update', on_click=update)
+
+    def get_series_0():
+        return screen.selenium.find_elements(By.CSS_SELECTOR, '.highcharts-series-0 .highcharts-point')
+
+    screen.open('/')
+    screen.wait(0.5)
+    before = [bar.size['width'] for bar in get_series_0()]
+    screen.click('Update')
+    screen.wait(0.5)
+    after = [bar.size['width'] for bar in get_series_0()]
+    assert before[0] < after[0]
+    assert before[1] < after[1]
+
+
+def test_adding_chart_series(screen: Screen):
+    chart = ui.chart({
+        'chart': {'type': 'bar'},
+        'xAxis': {'categories': ['A', 'B']},
+        'series': [],
+    }).classes('w-full h-64')
+
+    def add():
+        chart.options['series'].append({'name': 'X', 'data': [0.1, 0.2]})
+        chart.update()
+    ui.button('Add', on_click=add)
+
+    screen.open('/')
+    screen.click('Add')
+    screen.wait(0.5)
+    assert len(screen.selenium.find_elements(By.CSS_SELECTOR, '.highcharts-point')) == 3
+
+
+def test_removing_chart_series(screen: Screen):
+    chart = ui.chart({
+        'chart': {'type': 'bar'},
+        'xAxis': {'categories': ['A', 'B']},
+        'series': [
+            {'name': 'Alpha', 'data': [0.1, 0.2]},
+            {'name': 'Beta', 'data': [0.3, 0.4]},
+        ],
+    }).classes('w-full h-64')
+
+    def remove():
+        chart.options['series'].pop(0)
+        chart.update()
+    ui.button('Remove', on_click=remove)
+
+    screen.open('/')
+    screen.click('Remove')
+    screen.wait(0.5)
+    assert len(screen.selenium.find_elements(By.CSS_SELECTOR, '.highcharts-point')) == 3

+ 4 - 0
website/static/header.html

@@ -1,4 +1,8 @@
 <meta name="viewport" content="width=device-width, initial-scale=1" />
 <meta name="viewport" content="width=device-width, initial-scale=1" />
+<meta
+  name="description"
+  content="NiceGUI is an easy-to-use, Python-based UI framework, which shows up in your web browser. You can create buttons, dialogs, markdown, 3D scenes, plots and much more."
+/>
 
 
 <!-- https://realfavicongenerator.net/ -->
 <!-- https://realfavicongenerator.net/ -->
 <link rel="apple-touch-icon" sizes="180x180" href="favicon/apple-touch-icon.png" />
 <link rel="apple-touch-icon" sizes="180x180" href="favicon/apple-touch-icon.png" />