Browse Source

Merge branch 'main' into markdown_speedup

Rodja Trappe 2 years ago
parent
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
 - shared and individual web pages
 - 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
 - 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?
 
-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.
 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.

+ 3 - 3
examples/opencv_webcam/main.py

@@ -11,13 +11,13 @@ from nicegui import app, ui
 black_1px = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUGFdjYGBg+A8AAQQBAHAgZQsAAAAASUVORK5CYII='
 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)
 
 
 @app.get('/video/frame')
 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():
         return placeholder
     ret, frame = video_capture.read()
@@ -27,7 +27,7 @@ async def grab_video_frame() -> Response:
     jpeg = imencode_image.tobytes()
     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')
 # 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.

+ 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('Infinite Scroll', 'presents an infinitely scrolling image gallery')
             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'):
         link_target('why')

+ 2 - 0
nicegui/elements/chart.js

@@ -8,6 +8,8 @@ export default {
   },
   methods: {
     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);
     },
   },

+ 5 - 4
nicegui/elements/interactive_image.py

@@ -13,22 +13,23 @@ register_component('interactive_image', __file__, 'interactive_image.js')
 class InteractiveImage(SourceElement, ContentElement):
 
     def __init__(self, source: str = '', *,
+                 content: str = '',
                  on_mouse: Optional[Callable] = None, events: List[str] = ['click'], cross: bool = False) -> None:
         """Interactive Image
 
         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.
         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 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 events: list of JavaScript events to subscribe to (default: `['click']`)
         :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['cross'] = cross
 

+ 1 - 1
nicegui/elements/link.py

@@ -20,7 +20,7 @@ class Link(TextElement):
         """
         super().__init__(tag='a', text=text)
         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):

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

@@ -26,7 +26,6 @@ class ContentElement(Element):
         return self
 
     def set_content(self, content: str) -> None:
-        '''changes the content'''
         self.content = content
 
     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
 
     def set_source(self, source: str) -> None:
-        '''changes the image source'''
         self.source = source
 
     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:
         try:
             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:
             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="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/ -->
 <link rel="apple-touch-icon" sizes="180x180" href="favicon/apple-touch-icon.png" />