From afb3de0d30ba43583ec767df944b3f541b378ac2 Mon Sep 17 00:00:00 2001 From: Krzysztof Bielikowicz Date: Sat, 7 Feb 2026 01:30:46 +0000 Subject: [PATCH 1/5] Improvement: exclude default styling fields from component serialization Reduce payload size by not serializing styling fields when they match their class defaults. Uses _exclude_when_default ClassVar on ComponentInstance and StyledComponentInstance, resolved via model_fields for correct per-class defaults. Client-side fixes for bold/italic/underline, hug inheritance, and Stack hug defaulting. --- packages/dara-components/changelog.md | 5 ++ .../dara-components/js/common/stack/stack.tsx | 6 +- .../dara-components/js/common/text/text.tsx | 6 +- .../tests/python/common/test_anchor.py | 3 - .../python/common/test_base_component.py | 3 - .../tests/python/common/test_button.py | 3 - .../tests/python/common/test_carousel.py | 3 - .../python/common/test_checkbox_group.py | 3 - .../tests/python/common/test_code.py | 3 - .../tests/python/common/test_datepicker.py | 3 - .../tests/python/common/test_form.py | 3 - .../tests/python/common/test_form_page.py | 3 - .../tests/python/common/test_heading.py | 3 - .../tests/python/common/test_html_raw.py | 3 - .../tests/python/common/test_icon.py | 3 - .../tests/python/common/test_if_cmp.py | 3 - .../tests/python/common/test_image.py | 3 - .../tests/python/common/test_input.py | 3 - .../tests/python/common/test_label.py | 3 - .../tests/python/common/test_markdown.py | 3 - .../tests/python/common/test_modal.py | 3 - .../tests/python/common/test_paragraph.py | 3 - .../tests/python/common/test_progress_bar.py | 3 - .../tests/python/common/test_radio_group.py | 3 - .../tests/python/common/test_select.py | 3 - .../tests/python/common/test_slider.py | 3 - .../tests/python/common/test_spacer.py | 6 -- .../tests/python/common/test_stack.py | 12 +-- .../tests/python/common/test_switch.py | 3 - .../tests/python/common/test_table.py | 6 -- .../tests/python/common/test_text.py | 7 -- .../tests/python/common/test_textarea.py | 3 - .../tests/python/common/test_tooltip.py | 6 -- .../tests/python/plotting/test_bokeh.py | 3 - .../tests/python/smart/test_code_editor.py | 3 - .../python/smart/test_hierarchy_selector.py | 3 - packages/dara-core/changelog.md | 3 + packages/dara-core/dara/core/definitions.py | 83 +++++++++++++------ .../js/shared/utils/use-component-styles.tsx | 8 +- .../dara-core/tests/python/test_components.py | 6 -- 40 files changed, 83 insertions(+), 155 deletions(-) diff --git a/packages/dara-components/changelog.md b/packages/dara-components/changelog.md index c6e4f125c..cec2a3d10 100644 --- a/packages/dara-components/changelog.md +++ b/packages/dara-components/changelog.md @@ -2,6 +2,11 @@ title: Changelog --- +## NEXT + +- Fixed `Text` component to not emit unnecessary CSS properties for bold/italic/underline when they are at their defaults. +- Fixed `Stack` component to correctly default `hug` to `false` when the prop is not sent by the server. + ## 1.25.1 - Added `size` prop to `Select` component to control font-size of each sub-component diff --git a/packages/dara-components/js/common/stack/stack.tsx b/packages/dara-components/js/common/stack/stack.tsx index 4aaa4e224..c265a0cc3 100644 --- a/packages/dara-components/js/common/stack/stack.tsx +++ b/packages/dara-components/js/common/stack/stack.tsx @@ -45,7 +45,11 @@ const StyledStack = injectCss(styled.div` function Stack(props: StackProps, ref: ForwardedRef): JSX.Element { const [collapsed] = useVariable(props.collapsed); - const [style, css] = useComponentStyles(props); + // Default hug to false for Stack — when the server excludes hug=false (the default), + // props.hug is undefined. Without this, undefined would cause hug-inheritance from + // the parent DisplayCtx, which is incorrect for Stack's default behavior. + const resolvedProps = props.hug == null ? { ...props, hug: false } : props; + const [style, css] = useComponentStyles(resolvedProps); const stackContent = ( diff --git a/packages/dara-components/js/common/text/text.tsx b/packages/dara-components/js/common/text/text.tsx index 4f50a89a0..27d514b34 100644 --- a/packages/dara-components/js/common/text/text.tsx +++ b/packages/dara-components/js/common/text/text.tsx @@ -46,11 +46,11 @@ function Text(props: TextProps): JSX.Element { style={{ border: props?.border ?? 'none', color, - fontStyle: props.italic ? 'italic' : 'normal', - fontWeight: props.bold ? 'bold' : 'normal', + fontStyle: props.italic ? 'italic' : undefined, + fontWeight: props.bold ? 'bold' : undefined, marginRight: '0.1em', textAlign: props.align as any, - textDecoration: props.underline ? 'underline' : '', + textDecoration: props.underline ? 'underline' : undefined, ...style, }} > diff --git a/packages/dara-components/tests/python/common/test_anchor.py b/packages/dara-components/tests/python/common/test_anchor.py index 709397736..8142d4016 100644 --- a/packages/dara-components/tests/python/common/test_anchor.py +++ b/packages/dara-components/tests/python/common/test_anchor.py @@ -22,13 +22,10 @@ def test_serialization(self, _uid): expected_dict = { 'name': 'Anchor', 'props': { - 'bold': False, 'children': [t1.dict(exclude_none=True)], 'clean': False, 'href': '#anchor1', - 'italic': False, 'name': 'anchor1', - 'underline': False, 'new_tab': False, }, 'uid': 'uid', diff --git a/packages/dara-components/tests/python/common/test_base_component.py b/packages/dara-components/tests/python/common/test_base_component.py index 3ad609cf6..39318c226 100644 --- a/packages/dara-components/tests/python/common/test_base_component.py +++ b/packages/dara-components/tests/python/common/test_base_component.py @@ -32,9 +32,6 @@ def test_serialization(self, _uid): 'children': [t1.dict(exclude_none=True), t2.dict(exclude_none=True), t1.dict(exclude_none=True)], 'height': '10%', 'width': '10px', - 'bold': False, - 'italic': False, - 'underline': False, }, 'uid': 'uid', } diff --git a/packages/dara-components/tests/python/common/test_button.py b/packages/dara-components/tests/python/common/test_button.py index d776017fb..15fb008f0 100644 --- a/packages/dara-components/tests/python/common/test_button.py +++ b/packages/dara-components/tests/python/common/test_button.py @@ -22,15 +22,12 @@ def test_serialization(self, _uid): expected_dict = { 'name': 'Button', 'props': { - 'bold': False, 'children': [Text(text='Click Here').dict(exclude_none=True)], 'stop_click_propagation': True, - 'italic': False, 'onclick': action.dict(exclude_none=True), 'styling': ButtonStyle.PRIMARY.value, 'outline': False, 'position': 'relative', - 'underline': False, }, 'uid': str(test_uid), } diff --git a/packages/dara-components/tests/python/common/test_carousel.py b/packages/dara-components/tests/python/common/test_carousel.py index 861f56fd7..26bfd531c 100644 --- a/packages/dara-components/tests/python/common/test_carousel.py +++ b/packages/dara-components/tests/python/common/test_carousel.py @@ -15,11 +15,8 @@ def test_serialization(self, _uid): expected_dict = { 'name': 'Carousel', 'props': { - 'bold': False, 'children': [], - 'italic': False, 'items': [{'title': 'test', 'subtitle': 'item'}], - 'underline': False, }, 'uid': 'uid', } diff --git a/packages/dara-components/tests/python/common/test_checkbox_group.py b/packages/dara-components/tests/python/common/test_checkbox_group.py index 2e4f0b938..c0a492263 100644 --- a/packages/dara-components/tests/python/common/test_checkbox_group.py +++ b/packages/dara-components/tests/python/common/test_checkbox_group.py @@ -20,9 +20,7 @@ def test_serialization(self, _uid): expected_dict = { 'name': 'CheckboxGroup', 'props': { - 'bold': False, 'children': [], - 'italic': False, 'items': [ {'label': 'test1', 'value': 'test1'}, {'label': 'test2', 'value': 'test2'}, @@ -30,7 +28,6 @@ def test_serialization(self, _uid): ], 'list_styling': False, 'select_max': 2, - 'underline': False, 'value': value.dict(exclude_none=True), }, 'uid': str(test_uid), diff --git a/packages/dara-components/tests/python/common/test_code.py b/packages/dara-components/tests/python/common/test_code.py index bcaa039d6..ba31f5a09 100644 --- a/packages/dara-components/tests/python/common/test_code.py +++ b/packages/dara-components/tests/python/common/test_code.py @@ -20,9 +20,6 @@ def test_serialization(self, _uid): 'children': [], 'code': self.code, 'language': 'js', - 'bold': False, - 'italic': False, - 'underline': False, }, 'uid': 'uid', } diff --git a/packages/dara-components/tests/python/common/test_datepicker.py b/packages/dara-components/tests/python/common/test_datepicker.py index 062b51df7..7e9b28c92 100644 --- a/packages/dara-components/tests/python/common/test_datepicker.py +++ b/packages/dara-components/tests/python/common/test_datepicker.py @@ -23,15 +23,12 @@ def test_serialization(self, _uid): expected_dict = { 'name': 'Datepicker', 'props': { - 'bold': False, 'children': [], 'value': value.dict(exclude_none=True), 'date_format': 'dd/MM/yyyy', 'enable_time': False, - 'italic': False, 'range': False, 'select_close': True, - 'underline': False, }, 'uid': str(test_uid), } diff --git a/packages/dara-components/tests/python/common/test_form.py b/packages/dara-components/tests/python/common/test_form.py index 96919888d..40d401f50 100644 --- a/packages/dara-components/tests/python/common/test_form.py +++ b/packages/dara-components/tests/python/common/test_form.py @@ -22,10 +22,7 @@ def test_serialization(self, _uid): 'name': 'Form', 'props': { 'children': [s1.dict(exclude_none=True)], - 'bold': False, - 'italic': False, 'position': 'relative', - 'underline': False, 'value': form_state.dict(exclude_none=True), }, 'uid': str(test_uid), diff --git a/packages/dara-components/tests/python/common/test_form_page.py b/packages/dara-components/tests/python/common/test_form_page.py index 81e3adff5..2eaf901ea 100644 --- a/packages/dara-components/tests/python/common/test_form_page.py +++ b/packages/dara-components/tests/python/common/test_form_page.py @@ -20,9 +20,6 @@ def test_serialization(self, _uid): 'name': 'FormPage', 'props': { 'children': [s1.dict(exclude_none=True)], - 'bold': False, - 'italic': False, - 'underline': False, 'title': 'Page', }, 'uid': str(test_uid), diff --git a/packages/dara-components/tests/python/common/test_heading.py b/packages/dara-components/tests/python/common/test_heading.py index 2bd59ca4c..dcabb670b 100644 --- a/packages/dara-components/tests/python/common/test_heading.py +++ b/packages/dara-components/tests/python/common/test_heading.py @@ -14,12 +14,9 @@ def test_serialization(self, _uid): expected_dict = { 'name': 'Heading', 'props': { - 'bold': False, 'children': [], 'heading': 'Heading', - 'italic': False, 'level': 3, - 'underline': False, }, 'uid': 'uid', } diff --git a/packages/dara-components/tests/python/common/test_html_raw.py b/packages/dara-components/tests/python/common/test_html_raw.py index 5186e3d64..4592b564a 100644 --- a/packages/dara-components/tests/python/common/test_html_raw.py +++ b/packages/dara-components/tests/python/common/test_html_raw.py @@ -15,11 +15,8 @@ def test_serialization(self, _uid): expected_dict = { 'name': 'HtmlRaw', 'props': { - 'bold': False, 'children': [], 'html': html, - 'italic': False, - 'underline': False, }, 'uid': 'uid', } diff --git a/packages/dara-components/tests/python/common/test_icon.py b/packages/dara-components/tests/python/common/test_icon.py index 596f3f039..47abd0384 100644 --- a/packages/dara-components/tests/python/common/test_icon.py +++ b/packages/dara-components/tests/python/common/test_icon.py @@ -15,9 +15,6 @@ def test_serialization(self, _uid): 'name': 'Icon', 'props': { 'children': [], - 'bold': False, - 'italic': False, - 'underline': False, 'icon': 'wrench', 'color': 'red', }, diff --git a/packages/dara-components/tests/python/common/test_if_cmp.py b/packages/dara-components/tests/python/common/test_if_cmp.py index 531597d44..b3b183680 100644 --- a/packages/dara-components/tests/python/common/test_if_cmp.py +++ b/packages/dara-components/tests/python/common/test_if_cmp.py @@ -35,9 +35,6 @@ def test_serialization(self, _uid): }, 'false_children': [jsonable_encoder(t3, exclude_none=True)], 'true_children': [jsonable_encoder(t1, exclude_none=True), jsonable_encoder(t2, exclude_none=True)], - 'bold': False, - 'italic': False, - 'underline': False, }, 'uid': str(test_uid), } diff --git a/packages/dara-components/tests/python/common/test_image.py b/packages/dara-components/tests/python/common/test_image.py index c4a649aeb..dbf736546 100644 --- a/packages/dara-components/tests/python/common/test_image.py +++ b/packages/dara-components/tests/python/common/test_image.py @@ -25,9 +25,6 @@ def test_serialization(self, _uid): 'name': 'Image', 'props': { 'children': [], - 'bold': False, - 'italic': False, - 'underline': False, 'src': '/static/test.jpg', }, 'uid': 'uid', diff --git a/packages/dara-components/tests/python/common/test_input.py b/packages/dara-components/tests/python/common/test_input.py index a10d19b13..8b18072cb 100644 --- a/packages/dara-components/tests/python/common/test_input.py +++ b/packages/dara-components/tests/python/common/test_input.py @@ -20,10 +20,7 @@ def test_serialization(self, _uid): expected_dict = { 'name': 'Input', 'props': { - 'bold': False, 'children': [], - 'italic': False, - 'underline': False, 'value': value.dict(exclude_none=True), }, 'uid': str(test_uid), diff --git a/packages/dara-components/tests/python/common/test_label.py b/packages/dara-components/tests/python/common/test_label.py index f30dfb71e..bb4aab545 100644 --- a/packages/dara-components/tests/python/common/test_label.py +++ b/packages/dara-components/tests/python/common/test_label.py @@ -21,10 +21,7 @@ def test_serialization(self, _uid): 'name': 'Label', 'props': { 'children': [input.dict(exclude_none=True)], - 'bold': False, - 'italic': False, 'direction': Direction.VERTICAL.value, - 'underline': False, 'value': 'test label:', }, 'uid': str(test_uid), diff --git a/packages/dara-components/tests/python/common/test_markdown.py b/packages/dara-components/tests/python/common/test_markdown.py index 121deb49e..839038b55 100644 --- a/packages/dara-components/tests/python/common/test_markdown.py +++ b/packages/dara-components/tests/python/common/test_markdown.py @@ -25,12 +25,9 @@ def test_serialization(self, _uid): expected_dict = { 'name': 'Markdown', 'props': { - 'bold': False, 'children': [], 'html_raw': False, - 'italic': False, 'markdown': '# Heading\nSome text\n*italic words* **bold words**\n## List\n+ Sub-lists:\n- Marker\n* Another marker\n\n[link text](https://www.causalens.com/)', - 'underline': False, }, 'uid': 'uid', } diff --git a/packages/dara-components/tests/python/common/test_modal.py b/packages/dara-components/tests/python/common/test_modal.py index ba9fc0ba8..afd2465e0 100644 --- a/packages/dara-components/tests/python/common/test_modal.py +++ b/packages/dara-components/tests/python/common/test_modal.py @@ -38,10 +38,7 @@ async def on_closed(ctx: ActionCtx): 'name': 'Modal', 'props': { 'children': [t1.model_dump(exclude_none=True)], - 'bold': False, - 'italic': False, 'position': 'relative', - 'underline': False, 'show': show.model_dump(exclude_none=True), 'on_attempt_close': on_attempt_close().model_dump(exclude_none=True), 'on_closed': on_closed().model_dump(exclude_none=True), diff --git a/packages/dara-components/tests/python/common/test_paragraph.py b/packages/dara-components/tests/python/common/test_paragraph.py index 2dee4c304..734d40193 100644 --- a/packages/dara-components/tests/python/common/test_paragraph.py +++ b/packages/dara-components/tests/python/common/test_paragraph.py @@ -22,11 +22,8 @@ def test_serialization(self, _uid): expected_dict = { 'name': 'Paragraph', 'props': { - 'bold': False, 'children': [t1.dict(exclude_none=True), t2.dict(exclude_none=True)], - 'italic': False, 'position': 'relative', - 'underline': False, }, 'uid': 'uid', } diff --git a/packages/dara-components/tests/python/common/test_progress_bar.py b/packages/dara-components/tests/python/common/test_progress_bar.py index 8a35a4319..c71e9b2f0 100644 --- a/packages/dara-components/tests/python/common/test_progress_bar.py +++ b/packages/dara-components/tests/python/common/test_progress_bar.py @@ -14,12 +14,9 @@ def test_serialization(self, _uid): expected_dict = { 'name': 'ProgressBar', 'props': { - 'bold': False, 'children': [], 'progress': 50, - 'italic': False, 'small': True, - 'underline': False, }, 'uid': 'uid', } diff --git a/packages/dara-components/tests/python/common/test_radio_group.py b/packages/dara-components/tests/python/common/test_radio_group.py index 7a087d5e9..5b879b547 100644 --- a/packages/dara-components/tests/python/common/test_radio_group.py +++ b/packages/dara-components/tests/python/common/test_radio_group.py @@ -20,17 +20,14 @@ def test_serialization(self, _uid): expected_dict = { 'name': 'RadioGroup', 'props': { - 'bold': False, 'children': [], 'direction': 'vertical', - 'italic': False, 'items': [ {'label': 'test1', 'value': 'test1'}, {'label': 'test2', 'value': 'test2'}, {'label': 'test3', 'value': 'test3'}, ], 'list_styling': False, - 'underline': False, 'value': value.dict(exclude_none=True), }, 'uid': str(test_uid), diff --git a/packages/dara-components/tests/python/common/test_select.py b/packages/dara-components/tests/python/common/test_select.py index f65901e4f..7f5747e4f 100644 --- a/packages/dara-components/tests/python/common/test_select.py +++ b/packages/dara-components/tests/python/common/test_select.py @@ -20,9 +20,7 @@ def test_serialization(self, _uid): expected_dict = { 'name': 'Select', 'props': { - 'bold': False, 'children': [], - 'italic': False, 'items': [ {'label': 'test1', 'value': 'test1'}, {'label': 'test2', 'value': 'test2'}, @@ -31,7 +29,6 @@ def test_serialization(self, _uid): 'max_rows': 3, 'multiselect': False, 'searchable': False, - 'underline': False, 'value': value.dict(exclude_none=True), }, 'uid': str(test_uid), diff --git a/packages/dara-components/tests/python/common/test_slider.py b/packages/dara-components/tests/python/common/test_slider.py index 42db37286..e6c872ebc 100644 --- a/packages/dara-components/tests/python/common/test_slider.py +++ b/packages/dara-components/tests/python/common/test_slider.py @@ -129,13 +129,10 @@ def test_serialization(self, _uid): expected_dict = { 'name': 'Slider', 'props': { - 'bold': False, 'children': [], 'domain': [0.0, 1.0], - 'italic': False, 'rail_from_start': True, 'rail_to_end': False, - 'underline': False, 'value': value.dict(exclude_none=True), 'disable_input_alternative': False, 'onchange': onchange.dict(exclude_none=True), diff --git a/packages/dara-components/tests/python/common/test_spacer.py b/packages/dara-components/tests/python/common/test_spacer.py index 40d34590e..64fe5b8ee 100644 --- a/packages/dara-components/tests/python/common/test_spacer.py +++ b/packages/dara-components/tests/python/common/test_spacer.py @@ -14,13 +14,10 @@ def test_serialization(self, _uid): expected_dict = { 'name': 'Spacer', 'props': { - 'bold': False, 'children': [], 'inset': '0rem', - 'italic': False, 'line': False, 'size': '0.75rem', - 'underline': False, }, 'uid': 'uid', } @@ -31,13 +28,10 @@ def test_serialization(self, _uid): 'name': 'Spacer', 'props': { 'align': 'start', - 'bold': False, 'children': [], 'inset': '5%', - 'italic': False, 'line': True, 'size': '12px', - 'underline': False, }, 'uid': 'uid', } diff --git a/packages/dara-components/tests/python/common/test_stack.py b/packages/dara-components/tests/python/common/test_stack.py index 1be5d947a..a727f40d9 100644 --- a/packages/dara-components/tests/python/common/test_stack.py +++ b/packages/dara-components/tests/python/common/test_stack.py @@ -18,18 +18,18 @@ def test_serialization(self, _uid): test_stack = Stack(t1, t2) + # Default styling fields (bold, italic, underline, hug, etc.) are excluded + # from serialization when they match their defaults. Component-specific defaults + # like collapsed, direction, scroll are still included since the client + # does not apply matching defaults for those. expected_dict = { 'name': 'Stack', 'props': { - 'bold': False, 'children': [t1.dict(exclude_none=True), t2.dict(exclude_none=True)], 'collapsed': False, 'direction': Direction.VERTICAL.value, - 'italic': False, 'position': 'relative', 'scroll': False, - 'hug': False, - 'underline': False, }, 'uid': 'uid', } @@ -41,15 +41,11 @@ def test_serialization(self, _uid): 'name': 'Stack', 'props': { 'align': 'end', - 'bold': False, 'children': [t1.dict(exclude_none=True), t2.dict(exclude_none=True)], 'collapsed': False, 'direction': Direction.HORIZONTAL.value, - 'italic': False, 'position': 'relative', 'scroll': False, - 'hug': False, - 'underline': False, }, 'uid': 'uid', } diff --git a/packages/dara-components/tests/python/common/test_switch.py b/packages/dara-components/tests/python/common/test_switch.py index dbae85d30..e4de35006 100644 --- a/packages/dara-components/tests/python/common/test_switch.py +++ b/packages/dara-components/tests/python/common/test_switch.py @@ -23,10 +23,7 @@ def test_serialization(self, _uid): expected_dict = { 'name': 'Switch', 'props': { - 'bold': False, 'children': [], - 'italic': False, - 'underline': False, 'value': value.dict(exclude_none=True), }, 'uid': str(test_uid), diff --git a/packages/dara-components/tests/python/common/test_table.py b/packages/dara-components/tests/python/common/test_table.py index e8e271da0..04d30030b 100644 --- a/packages/dara-components/tests/python/common/test_table.py +++ b/packages/dara-components/tests/python/common/test_table.py @@ -38,10 +38,7 @@ def test_serialization(self, _uid): 'name': 'Table', 'props': { 'children': [], - 'bold': False, 'include_index': True, - 'italic': False, - 'underline': False, 'columns': [ { 'col_id': 'name', @@ -78,10 +75,7 @@ def test_serialization(self, _uid): 'name': 'Table', 'props': { 'children': [], - 'bold': False, - 'italic': False, 'include_index': True, - 'underline': False, 'columns': [ { 'col_id': 'col1', diff --git a/packages/dara-components/tests/python/common/test_text.py b/packages/dara-components/tests/python/common/test_text.py index c4e1b89d9..cb00169c8 100644 --- a/packages/dara-components/tests/python/common/test_text.py +++ b/packages/dara-components/tests/python/common/test_text.py @@ -14,13 +14,9 @@ def test_serialization(self, _uid): expected_dict = { 'name': 'Text', 'props': { - 'align': 'left', - 'bold': False, 'children': [], 'formatted': False, - 'italic': False, 'text': 'Some Text', - 'underline': False, }, 'uid': 'uid', } @@ -32,10 +28,7 @@ def test_serialization(self, _uid): 'props': { 'children': [], 'align': 'center', - 'bold': False, 'formatted': False, - 'italic': False, - 'underline': False, 'width': '10px', 'text': 'Some Text', }, diff --git a/packages/dara-components/tests/python/common/test_textarea.py b/packages/dara-components/tests/python/common/test_textarea.py index 7254305f2..92d8a48a4 100644 --- a/packages/dara-components/tests/python/common/test_textarea.py +++ b/packages/dara-components/tests/python/common/test_textarea.py @@ -24,11 +24,8 @@ def test_serialization(self, _uid): 'name': 'Textarea', 'props': { 'autofocus': False, - 'bold': False, 'children': [], - 'italic': False, 'value': value.dict(exclude_none=True), - 'underline': False, }, 'uid': str(test_uid), } diff --git a/packages/dara-components/tests/python/common/test_tooltip.py b/packages/dara-components/tests/python/common/test_tooltip.py index 20b90828b..645fe4ce2 100644 --- a/packages/dara-components/tests/python/common/test_tooltip.py +++ b/packages/dara-components/tests/python/common/test_tooltip.py @@ -18,13 +18,10 @@ def test_serialization(self, _uid): expected_dict = { 'name': 'Tooltip', 'props': { - 'bold': False, 'children': [Stack(t1).dict(exclude_none=True)], 'content': 'Hover Content', - 'italic': False, 'placement': 'auto', 'styling': 'default', - 'underline': False, }, 'uid': 'uid', } @@ -36,13 +33,10 @@ def test_serialization(self, _uid): expected_dict = { 'name': 'Tooltip', 'props': { - 'bold': False, 'children': [t1_stack.dict(exclude_none=True)], 'content': t2.dict(exclude_none=True), - 'italic': False, 'placement': 'bottom', 'styling': 'default', - 'underline': False, }, 'uid': 'uid', } diff --git a/packages/dara-components/tests/python/plotting/test_bokeh.py b/packages/dara-components/tests/python/plotting/test_bokeh.py index da023f9bc..3158560e9 100644 --- a/packages/dara-components/tests/python/plotting/test_bokeh.py +++ b/packages/dara-components/tests/python/plotting/test_bokeh.py @@ -27,9 +27,6 @@ def test_serialization(self, _uid): 'name': 'Bokeh', 'props': { 'document': cmp.document, - 'bold': False, - 'italic': False, - 'underline': False, }, 'uid': str(test_uid), } diff --git a/packages/dara-components/tests/python/smart/test_code_editor.py b/packages/dara-components/tests/python/smart/test_code_editor.py index 76dd09e0f..164d7fd16 100644 --- a/packages/dara-components/tests/python/smart/test_code_editor.py +++ b/packages/dara-components/tests/python/smart/test_code_editor.py @@ -20,9 +20,6 @@ def test_serialization(self, _uid): 'name': 'CodeEditor', 'props': { 'script': script.dict(exclude_none=True), - 'bold': False, - 'italic': False, - 'underline': False, }, 'uid': str(test_uid), } diff --git a/packages/dara-components/tests/python/smart/test_hierarchy_selector.py b/packages/dara-components/tests/python/smart/test_hierarchy_selector.py index eb704be97..109cb9327 100644 --- a/packages/dara-components/tests/python/smart/test_hierarchy_selector.py +++ b/packages/dara-components/tests/python/smart/test_hierarchy_selector.py @@ -22,9 +22,6 @@ def test_serialization(self, _uid): 'props': { 'allow_category_select': True, 'allow_leaf_select': True, - 'bold': False, - 'italic': False, - 'underline': False, 'hierarchy': { 'children': [{'id': 'node2', 'label': 'node2', 'weight': 0.0}], 'id': 'node1', diff --git a/packages/dara-core/changelog.md b/packages/dara-core/changelog.md index b745b9b1d..76e01dac7 100644 --- a/packages/dara-core/changelog.md +++ b/packages/dara-core/changelog.md @@ -4,6 +4,9 @@ title: Changelog ## NEXT +- Improved component serialization to exclude styling fields at their default values, significantly reducing payload size. Fields like `bold`, `italic`, `underline`, `align`, `background`, `hug`, and other styling props are no longer sent when they match their defaults. This is controlled via `_exclude_when_default` ClassVar on `ComponentInstance` and `StyledComponentInstance`, which subclasses can extend. +- Fixed `useComponentStyles` to not emit unnecessary CSS properties (`fontStyle: 'normal'`, `fontWeight: 'normal'`, `textDecoration: 'none'`) when styling props are not set. +- Fixed `flexStyles` hug-inheritance logic to correctly treat missing `hug` prop the same as `null` (inherit from parent context) rather than `false` (explicit no-hug). - Added cleanup support to `on_startup`: startup functions can now optionally return a cleanup callable which is invoked during application shutdown in reverse order (LIFO). Both sync and async cleanup functions are supported. ## 1.25.3 diff --git a/packages/dara-core/dara/core/definitions.py b/packages/dara-core/dara/core/definitions.py index 8f0cd351b..cfecc04c9 100644 --- a/packages/dara-core/dara/core/definitions.py +++ b/packages/dara-core/dara/core/definitions.py @@ -41,6 +41,7 @@ field_validator, model_serializer, ) +from pydantic_core import PydanticUndefined from dara.core.base_definitions import Action, ComponentType from dara.core.base_definitions import DaraBaseModel as BaseModel @@ -118,6 +119,15 @@ class ComponentInstance(BaseModel): Definition of a Component Instance """ + _exclude_when_default: ClassVar[frozenset[str]] = frozenset( + {'raw_css', 'track_progress', 'error_handler', 'fallback', 'id_', 'for_'} + ) + """ + Fields listed here will be excluded from serialization when their value matches the field's default. + This reduces payload size by not sending default values the client already knows about. + Subclasses can extend this set. + """ + uid: str = Field(default_factory=lambda: str(uuid.uuid4())) js_module: ClassVar[str | None] = None @@ -222,32 +232,24 @@ def ser_model(self, nxt: SerializerFunctionWrapHandler) -> dict: props.pop('uid') - # Exclude raw_css if not set - if 'raw_css' in props and props.get('raw_css') is None: - props.pop('raw_css') - elif isinstance(self.raw_css, CSSProperties): - # If it's an instance of CSSProperties, serialize but exclude none + # Special handling for raw_css: CSSProperties needs exclude_none serialization + if isinstance(self.raw_css, CSSProperties): props['raw_css'] = self.raw_css.model_dump(exclude_none=True) - # Exclude track_progress if not set - if 'track_progress' in props and props.get('track_progress') is False: - props.pop('track_progress') - - # Exclude error handler if not set - if 'error_handler' in props and props.get('error_handler') is None: - props.pop('error_handler') - - # Exclude fallback if not set - if 'fallback' in props and props.get('fallback') is None: - props.pop('fallback') - - # Exclude id_ if not set - if 'id_' in props and props.get('id_') is None: - props.pop('id_') - - # Exclude for_ if not set - if 'for_' in props and props.get('for_') is None: - props.pop('for_') + # Exclude fields at their default values to reduce serialized payload size. + # The client handles missing (undefined) fields by applying matching defaults. + # Uses model_fields to resolve per-class defaults (e.g. LayoutComponent overrides + # position='relative' while StyledComponentInstance defaults to None). + exclude_fields = getattr(type(self), '_exclude_when_default', frozenset()) + for field_name in exclude_fields: + if field_name not in props: + continue + field_info = type(self).model_fields.get(field_name) + if field_info is None or field_info.default is PydanticUndefined: + continue + default = field_info.default.value if isinstance(field_info.default, Enum) else field_info.default + if props[field_name] == default: + props.pop(field_name) return { 'name': self.py_component or type(self).__name__, @@ -330,6 +332,39 @@ class StyledComponentInstance(ComponentInstance): :param width: the width of the component, can be an number, which will be converted to pixels, or a string """ + _exclude_when_default: ClassVar[frozenset[str]] = ComponentInstance._exclude_when_default | frozenset( + { + 'align', + 'background', + 'basis', + 'bold', + 'border', + 'border_radius', + 'children', + 'color', + 'font', + 'font_size', + 'gap', + 'grow', + 'height', + 'hug', + 'italic', + 'margin', + 'max_height', + 'max_width', + 'min_height', + 'min_width', + 'overflow', + 'padding', + # Note: 'position' is intentionally excluded from this set. + # LayoutComponent overrides it to 'relative' and client layout components + # rely on receiving it for proper stacking context. + 'shrink', + 'underline', + 'width', + } + ) + align: AlignItems | None = None background: str | None = None basis: int | str | bool | None = None diff --git a/packages/dara-core/js/shared/utils/use-component-styles.tsx b/packages/dara-core/js/shared/utils/use-component-styles.tsx index be6a5145b..fa2fc5972 100644 --- a/packages/dara-core/js/shared/utils/use-component-styles.tsx +++ b/packages/dara-core/js/shared/utils/use-component-styles.tsx @@ -49,7 +49,7 @@ function flexStyles(props: StyledComponentProps, displayCtx: DisplayCtxValue, us } // If hug is set, then the flex-basis should be set to content // Otherwise we check if the parent is a component that has hug set to true, and its children should inherit - if (props.hug || (props.hug !== false && displayCtx.hug)) { + if (props.hug || (props.hug == null && displayCtx.hug)) { flexBasis ??= 'content'; flexShrink ??= '1'; flexGrow ??= '0'; @@ -97,8 +97,8 @@ export default function useComponentStyles( color: props.color, fontFamily: props.font, fontSize: props.font_size, - fontStyle: props.italic ? 'italic' : 'normal', - fontWeight: props.bold ? 'bold' : 'normal', + fontStyle: props.italic ? 'italic' : undefined, + fontWeight: props.bold ? 'bold' : undefined, gap: props.gap, height: props.height, margin: props.margin, @@ -109,7 +109,7 @@ export default function useComponentStyles( overflow: props.overflow, padding: props.padding, position: props.position, - textDecoration: props.underline ? 'underline' : 'none', + textDecoration: props.underline ? 'underline' : undefined, width: props.width, ...flexProps, ...rawStyles, diff --git a/packages/dara-core/tests/python/test_components.py b/packages/dara-core/tests/python/test_components.py index 090fcaf64..ce8df6109 100644 --- a/packages/dara-core/tests/python/test_components.py +++ b/packages/dara-core/tests/python/test_components.py @@ -64,18 +64,12 @@ class TestComponent(StyledComponentInstance): 'uid': instance.uid, 'props': { 'foo': 'bar', - 'underline': False, - 'bold': False, - 'italic': False, 'children': [ { 'name': 'TestComponent', 'uid': instance.children[0].uid, 'props': { 'foo': 'baz', - 'bold': False, - 'underline': False, - 'italic': False, }, }, ], From 930c83187ecc64721a412759c6d8d9b27c34a0b0 Mon Sep 17 00:00:00 2001 From: Krzysztof Bielikowicz Date: Sat, 7 Feb 2026 02:10:18 +0000 Subject: [PATCH 2/5] Improvement: remove children from exclude set, fix Stack hug via Python-side override, simplify Text align default --- packages/dara-components/changelog.md | 3 +-- .../dara-components/dara/components/common/stack.py | 6 ++++++ .../dara-components/dara/components/common/text.py | 2 +- packages/dara-components/js/common/stack/stack.tsx | 6 +----- .../dara-components/tests/python/common/test_stack.py | 10 ++++++---- packages/dara-core/changelog.md | 4 +--- packages/dara-core/dara/core/definitions.py | 1 - 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/dara-components/changelog.md b/packages/dara-components/changelog.md index cec2a3d10..6913479a8 100644 --- a/packages/dara-components/changelog.md +++ b/packages/dara-components/changelog.md @@ -4,8 +4,7 @@ title: Changelog ## NEXT -- Fixed `Text` component to not emit unnecessary CSS properties for bold/italic/underline when they are at their defaults. -- Fixed `Stack` component to correctly default `hug` to `false` when the prop is not sent by the server. +- Fixed `Text` component to not emit unnecessary inline CSS properties when styling props are at their defaults. ## 1.25.1 diff --git a/packages/dara-components/dara/components/common/stack.py b/packages/dara-components/dara/components/common/stack.py index 268e07961..2c47a481a 100644 --- a/packages/dara-components/dara/components/common/stack.py +++ b/packages/dara-components/dara/components/common/stack.py @@ -16,6 +16,7 @@ """ import logging +from typing import ClassVar from dara.components.common.base_component import LayoutComponent from dara.core.definitions import ComponentInstance @@ -134,6 +135,11 @@ class Stack(LayoutComponent): :param scroll: Whether to scroll the content of the stack, defaults to False """ + # Override to keep 'hug' in the serialized payload. Stack defaults hug=False + # which differs from StyledComponentInstance's hug=None. If excluded, the JS client + # receives undefined and would incorrectly inherit hug from a parent Grid context. + _exclude_when_default: ClassVar[frozenset[str]] = LayoutComponent._exclude_when_default - frozenset({'hug'}) + collapsed: Variable[bool] | bool = False direction: Direction = Direction.VERTICAL hug: bool | None = False diff --git a/packages/dara-components/dara/components/common/text.py b/packages/dara-components/dara/components/common/text.py index 2e2a342b1..eae8a5076 100644 --- a/packages/dara-components/dara/components/common/text.py +++ b/packages/dara-components/dara/components/common/text.py @@ -52,7 +52,7 @@ class Text(ContentComponent): """ text: str | ClientVariable - align: str | None = 'left' # type: ignore # this is actually textAlign not align-items + align: str | None = None # type: ignore # this is actually textAlign not align-items formatted: bool = False @field_validator('text') diff --git a/packages/dara-components/js/common/stack/stack.tsx b/packages/dara-components/js/common/stack/stack.tsx index c265a0cc3..4aaa4e224 100644 --- a/packages/dara-components/js/common/stack/stack.tsx +++ b/packages/dara-components/js/common/stack/stack.tsx @@ -45,11 +45,7 @@ const StyledStack = injectCss(styled.div` function Stack(props: StackProps, ref: ForwardedRef): JSX.Element { const [collapsed] = useVariable(props.collapsed); - // Default hug to false for Stack — when the server excludes hug=false (the default), - // props.hug is undefined. Without this, undefined would cause hug-inheritance from - // the parent DisplayCtx, which is incorrect for Stack's default behavior. - const resolvedProps = props.hug == null ? { ...props, hug: false } : props; - const [style, css] = useComponentStyles(resolvedProps); + const [style, css] = useComponentStyles(props); const stackContent = ( diff --git a/packages/dara-components/tests/python/common/test_stack.py b/packages/dara-components/tests/python/common/test_stack.py index a727f40d9..ec83423ce 100644 --- a/packages/dara-components/tests/python/common/test_stack.py +++ b/packages/dara-components/tests/python/common/test_stack.py @@ -18,16 +18,17 @@ def test_serialization(self, _uid): test_stack = Stack(t1, t2) - # Default styling fields (bold, italic, underline, hug, etc.) are excluded - # from serialization when they match their defaults. Component-specific defaults - # like collapsed, direction, scroll are still included since the client - # does not apply matching defaults for those. + # Default styling fields (bold, italic, underline, etc.) are excluded from + # serialization when they match their defaults. Stack overrides _exclude_when_default + # to keep 'hug' so the client always receives an explicit value (preventing + # incorrect hug-inheritance from parent Grid contexts). expected_dict = { 'name': 'Stack', 'props': { 'children': [t1.dict(exclude_none=True), t2.dict(exclude_none=True)], 'collapsed': False, 'direction': Direction.VERTICAL.value, + 'hug': False, 'position': 'relative', 'scroll': False, }, @@ -44,6 +45,7 @@ def test_serialization(self, _uid): 'children': [t1.dict(exclude_none=True), t2.dict(exclude_none=True)], 'collapsed': False, 'direction': Direction.HORIZONTAL.value, + 'hug': False, 'position': 'relative', 'scroll': False, }, diff --git a/packages/dara-core/changelog.md b/packages/dara-core/changelog.md index 76e01dac7..cf6287e0b 100644 --- a/packages/dara-core/changelog.md +++ b/packages/dara-core/changelog.md @@ -4,9 +4,7 @@ title: Changelog ## NEXT -- Improved component serialization to exclude styling fields at their default values, significantly reducing payload size. Fields like `bold`, `italic`, `underline`, `align`, `background`, `hug`, and other styling props are no longer sent when they match their defaults. This is controlled via `_exclude_when_default` ClassVar on `ComponentInstance` and `StyledComponentInstance`, which subclasses can extend. -- Fixed `useComponentStyles` to not emit unnecessary CSS properties (`fontStyle: 'normal'`, `fontWeight: 'normal'`, `textDecoration: 'none'`) when styling props are not set. -- Fixed `flexStyles` hug-inheritance logic to correctly treat missing `hug` prop the same as `null` (inherit from parent context) rather than `false` (explicit no-hug). +- Improved component serialization to exclude styling fields at their default values, significantly reducing payload size. - Added cleanup support to `on_startup`: startup functions can now optionally return a cleanup callable which is invoked during application shutdown in reverse order (LIFO). Both sync and async cleanup functions are supported. ## 1.25.3 diff --git a/packages/dara-core/dara/core/definitions.py b/packages/dara-core/dara/core/definitions.py index cfecc04c9..1c6342342 100644 --- a/packages/dara-core/dara/core/definitions.py +++ b/packages/dara-core/dara/core/definitions.py @@ -340,7 +340,6 @@ class StyledComponentInstance(ComponentInstance): 'bold', 'border', 'border_radius', - 'children', 'color', 'font', 'font_size', From 34f84a32b93dd0d4d1112deeaa0eb8e5d2fcf910 Mon Sep 17 00:00:00 2001 From: Krzysztof Bielikowicz Date: Sat, 7 Feb 2026 15:02:05 +0000 Subject: [PATCH 3/5] Skip all none values, add demo project --- .github/workflows/tests.yml | 40 +- packages/dara-components/changelog.md | 1 + .../dara-components/js/common/card/card.tsx | 6 +- packages/dara-core/changelog.md | 5 +- packages/dara-core/dara/core/definitions.py | 14 +- .../dara-core/tests/python/test_components.py | 1 - .../python/test_tabular_server_variable.py | 2 +- packages/demo-app/.gitignore | 2 + packages/demo-app/README.md | 28 + packages/demo-app/demo_app/main.py | 34 + .../demo_app/pages/components_page.py | 184 ++ .../demo-app/demo_app/pages/intro_page.py | 49 + .../demo-app/demo_app/utils/components.py | 2280 +++++++++++++++++ packages/demo-app/poetry.toml | 3 + packages/demo-app/pyproject.toml | 23 + packages/demo-app/static/dara_light.svg | 15 + poetry.lock | 27 +- pyproject.toml | 52 +- 18 files changed, 2707 insertions(+), 59 deletions(-) create mode 100644 packages/demo-app/.gitignore create mode 100644 packages/demo-app/README.md create mode 100644 packages/demo-app/demo_app/main.py create mode 100644 packages/demo-app/demo_app/pages/components_page.py create mode 100644 packages/demo-app/demo_app/pages/intro_page.py create mode 100644 packages/demo-app/demo_app/utils/components.py create mode 100644 packages/demo-app/poetry.toml create mode 100644 packages/demo-app/pyproject.toml create mode 100644 packages/demo-app/static/dara_light.svg diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index da4370eaf..f31904ba6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -247,23 +247,23 @@ jobs: path: packages/dara-core/cypress/screenshots retention-days: 30 - notify: - needs: [python-lint, js-lint, python-tests, js-tests, e2e-tests] - runs-on: ubuntu-latest - if: always() - steps: - - uses: 8398a7/action-slack@v3 - with: - status: custom - fields: workflow,job,commit,repo,ref,author,took - custom_payload: | - { - username: 'action-slack', - attachments: [{ - color: '${{ contains(needs.*.result, 'failure') }}' === 'true' ? 'danger' : '${{ contains(needs.*.result, 'cancelled') }}' === 'true' ? 'warning' : 'good', - text: `${process.env.AS_WORKFLOW}\n${process.env.AS_JOB} (${process.env.AS_COMMIT}) of ${process.env.AS_REPO}@${process.env.AS_REF} by ${process.env.AS_AUTHOR} ${{ contains(needs.*.result, 'failure') && 'failed' || contains(needs.*.result, 'cancelled') && 'cancelled' || 'succeeded' }} in ${process.env.AS_TOOK}`, - }] - } - env: - SLACK_WEBHOOK_URL: "${{ secrets.SLACK_BUILD_CHANNEL_WEBHOOK }}" - if: ${{ github.actor != 'dependabot[bot]' }} + # notify: + # needs: [python-lint, js-lint, python-tests, js-tests, e2e-tests] + # runs-on: ubuntu-latest + # if: always() + # steps: + # - uses: 8398a7/action-slack@v3 + # with: + # status: custom + # fields: workflow,job,commit,repo,ref,author,took + # custom_payload: | + # { + # username: 'action-slack', + # attachments: [{ + # color: '${{ contains(needs.*.result, 'failure') }}' === 'true' ? 'danger' : '${{ contains(needs.*.result, 'cancelled') }}' === 'true' ? 'warning' : 'good', + # text: `${process.env.AS_WORKFLOW}\n${process.env.AS_JOB} (${process.env.AS_COMMIT}) of ${process.env.AS_REPO}@${process.env.AS_REF} by ${process.env.AS_AUTHOR} ${{ contains(needs.*.result, 'failure') && 'failed' || contains(needs.*.result, 'cancelled') && 'cancelled' || 'succeeded' }} in ${process.env.AS_TOOK}`, + # }] + # } + # env: + # SLACK_WEBHOOK_URL: "${{ secrets.SLACK_BUILD_CHANNEL_WEBHOOK }}" + # if: ${{ github.actor != 'dependabot[bot]' }} diff --git a/packages/dara-components/changelog.md b/packages/dara-components/changelog.md index 6913479a8..9ec897c76 100644 --- a/packages/dara-components/changelog.md +++ b/packages/dara-components/changelog.md @@ -4,6 +4,7 @@ title: Changelog ## NEXT +- Fixed `Card` component to handle missing `title`/`subtitle` props correctly when `None` values are excluded from serialization. - Fixed `Text` component to not emit unnecessary inline CSS properties when styling props are at their defaults. ## 1.25.1 diff --git a/packages/dara-components/js/common/card/card.tsx b/packages/dara-components/js/common/card/card.tsx index 94cc5ba6b..708253b7d 100644 --- a/packages/dara-components/js/common/card/card.tsx +++ b/packages/dara-components/js/common/card/card.tsx @@ -102,11 +102,11 @@ function Card(props: CardProps): JSX.Element { return ( {title && {title}} - {subtitle && {subtitle}} + {subtitle && {subtitle}} dict: # Uses model_fields to resolve per-class defaults (e.g. LayoutComponent overrides # position='relative' while StyledComponentInstance defaults to None). exclude_fields = getattr(type(self), '_exclude_when_default', frozenset()) - for field_name in exclude_fields: + for field_name, field_info in type(self).model_fields.items(): if field_name not in props: continue - field_info = type(self).model_fields.get(field_name) - if field_info is None or field_info.default is PydanticUndefined: + if field_info.default is PydanticUndefined: continue default = field_info.default.value if isinstance(field_info.default, Enum) else field_info.default - if props[field_name] == default: + # Two exclusion rules, both pop the field: + # 1. _exclude_when_default fields: exclude when value matches the declared default + # 2. All fields: exclude None when the field's default is None. Safe because the + # client treats missing (undefined) and null equivalently for Optional fields. + # Does NOT strip None when default is non-None (e.g. track_progress: bool | None = False). + if (field_name in exclude_fields and props[field_name] == default) or ( + default is None and props[field_name] is None + ): props.pop(field_name) return { diff --git a/packages/dara-core/tests/python/test_components.py b/packages/dara-core/tests/python/test_components.py index ce8df6109..fee66c0e5 100644 --- a/packages/dara-core/tests/python/test_components.py +++ b/packages/dara-core/tests/python/test_components.py @@ -122,7 +122,6 @@ def TestComponent(): 'fallback': jsonable_encoder(fallback), 'func_name': 'TestComponent', 'dynamic_kwargs': {}, - 'polling_interval': None, }, 'uid': component.uid, } diff --git a/packages/dara-core/tests/python/test_tabular_server_variable.py b/packages/dara-core/tests/python/test_tabular_server_variable.py index 526b5cdeb..e751457af 100644 --- a/packages/dara-core/tests/python/test_tabular_server_variable.py +++ b/packages/dara-core/tests/python/test_tabular_server_variable.py @@ -646,4 +646,4 @@ def TestBasicComp(input_val: DataFrame, input_val_2: int): ) # Should return (2 + len(df)) + len(df), so (2 + 5 + 5) = 12 - assert data == {'name': 'MockComponent', 'props': {'text': '12', 'action': None}, 'uid': 'uid'} + assert data == {'name': 'MockComponent', 'props': {'text': '12'}, 'uid': 'uid'} diff --git a/packages/demo-app/.gitignore b/packages/demo-app/.gitignore new file mode 100644 index 000000000..637e76541 --- /dev/null +++ b/packages/demo-app/.gitignore @@ -0,0 +1,2 @@ +dist +.env diff --git a/packages/demo-app/README.md b/packages/demo-app/README.md new file mode 100644 index 000000000..7f4f52eed --- /dev/null +++ b/packages/demo-app/README.md @@ -0,0 +1,28 @@ +# Demo App + + + +## How to run the app + +To run the application you can use the following command: + +```bash +poetry run dara start +``` + +For development purposes it is often useful to add the `--reload` flag which will automatically reload the application when changes are made to any of the Python files. + +By default this will load the config from the `config` variable in `./demo_app/main.py` module. +To see the list of available config options you can use the `--help` flag: + +```bash +poetry run dara start --help +``` + +To see other available commands you can run: + +```bash +poetry run dara +``` + + diff --git a/packages/demo-app/demo_app/main.py b/packages/demo-app/demo_app/main.py new file mode 100644 index 000000000..d3d0f349e --- /dev/null +++ b/packages/demo-app/demo_app/main.py @@ -0,0 +1,34 @@ +from demo_app.pages.components_page import components_page +from demo_app.pages.intro_page import intro_page + +from dara.core import ConfigurationBuilder, MenuLink, Outlet, SideBarFrame +from dara.core.css import get_icon + +from dara.components import Icon, Stack, Text + +# Create the configuration builder +config = ConfigurationBuilder() + +# Root layout that displays a sidebar with links to the two pages +def RootLayout(): + return SideBarFrame( + content=Outlet(), + side_bar=Stack( + MenuLink( + Icon(icon=get_icon('newspaper')), + Text('Welcome'), + to='/', + ), + MenuLink( + Icon(icon=get_icon('spell-check')), + Text('A-Z Components'), + to='/components', + ), + ) + ) + +# Add the layout and pages to the configuration +root = config.router.add_layout(content=RootLayout) +root.add_page(path='/', content=intro_page) +root.add_page(path='/components', content=components_page) + diff --git a/packages/demo-app/demo_app/pages/components_page.py b/packages/demo-app/demo_app/pages/components_page.py new file mode 100644 index 000000000..369f5d58e --- /dev/null +++ b/packages/demo-app/demo_app/pages/components_page.py @@ -0,0 +1,184 @@ +from typing import List + +from demo_app.utils.components import ( + accordion, + anchor, + bokeh, + bullet_list, + button, + button_bar, + card, + carousel, + causal_graph_viewer, + causal_graph_viewer_planar, + checkbox_group, + code, + component_select_list, + datepicker, + edge_encoder, + form, + grid, + heading, + html_raw, + icon, + image, + input, + label, + markdown, + matplotlib, + modal, + node_hierarchy_builder, + overlay, + paragraph, + plotly, + progress_bar, + radio_group, + seaborn, + select, + slider, + spacer, + stack, + switch, + tabbed_card, + table, + text, + textarea, + tooltip, +) + +from dara.components import Button, Card, Heading, Label, Select, Spacer, Stack, Text +from dara.core import ComponentInstance, Variable, py_component + +dara_graphs_map = { + 'CausalGraphViewer': causal_graph_viewer, + 'CausalGraphViewer (Planar Layout)': causal_graph_viewer_planar, + 'EdgeEncoder': edge_encoder, + 'NodeHierarchyBuilder': node_hierarchy_builder, +} + +dara_plotting_map = { + 'Bokeh': bokeh, + 'Matplotlib': matplotlib, + 'Plotly': plotly, + 'Seaborn': seaborn, +} + +dara_components_map = { + 'Accordion': accordion, + 'Anchor': anchor, + 'Button': button, + 'BulletList': bullet_list, + 'ButtonBar': button_bar, + 'Card': card, + 'Carousel': carousel, + 'CheckboxGroup': checkbox_group, + 'Code': code, + 'ComponentSelectList': component_select_list, + 'Datepicker': datepicker, + 'Form': form, + 'Grid': grid, + 'Heading': heading, + 'HtmlRaw': html_raw, + 'Icon': icon, + 'Image': image, + 'Input': input, + 'Label': label, + 'Markdown': markdown, + 'Modal': modal, + 'Overlay': overlay, + 'Paragraph': paragraph, + 'ProgressBar': progress_bar, + 'RadioGroup': radio_group, + 'Select': select, + 'Slider': slider, + 'Spacer': spacer, + 'Stack': stack, + 'Switch': switch, + 'TabbedCard': tabbed_card, + 'Table': table, + 'Text': text, + 'Textarea': textarea, + 'Tooltip': tooltip, +} + +dara_graphs = list(dara_graphs_map.keys()) +dara_plotting = list(dara_plotting_map.keys()) +dara_components = list(dara_components_map.keys()) + +all_dara_components = dara_graphs + dara_plotting + dara_components + +select_var = Variable([]) + + +def component_card(name: str, content: ComponentInstance) -> ComponentInstance: + return Card(content, title=name) + + +def component_solo(name: str, content: ComponentInstance) -> ComponentInstance: + return Stack(Heading(name, level=3, padding='0 1rem'), content) + +def italic_text(text: str): + return Text( + text, + padding='0px', + raw_css={'font-style': 'italic'}, + ) + +@py_component +def components_to_show(select_val: List) -> ComponentInstance: + components = Stack() + + # show all components if none selected + show_components = select_val + if len(select_val) == 0: + show_components = all_dara_components + + if set(show_components).intersection(dara_graphs): + components.append(Spacer()) + components.append(Heading('Graph Components', level=2)) + for graph in show_components: + if graph in dara_graphs: + components.append(component_card(graph, dara_graphs_map[graph]())) + + if set(show_components).intersection(dara_plotting): + components.append(Spacer()) + components.append(Heading('Plotting Components', level=2)) + for plot in show_components: + if plot in dara_plotting: + components.append(component_card(plot, dara_plotting_map[plot]())) + + if set(show_components).intersection(dara_components): + components.append(Spacer()) + components.append(Heading('Common Components', level=2)) + for component in show_components: + if component in dara_components: + if component in ['Card', 'TabbedCard']: + components.append(component_solo(component, dara_components_map[component]())) + else: + components.append(component_card(component, dara_components_map[component]())) + + return components + + +def components_page() -> ComponentInstance: + return Stack( + Stack( + Heading('A-Z Components'), + Label( + Select( + value=select_var, + items=all_dara_components, + multiselect=True, + ), + value='Select component(s) to show:', + ), + Stack( + Button('Show all', outline=True, onclick=select_var.update(value=all_dara_components)), + Button('Clear', outline=True, onclick=select_var.update(value=[])), + direction='horizontal', + ), + hug=True + ), + Spacer(line=True), + components_to_show(select_var), + ) \ No newline at end of file diff --git a/packages/demo-app/demo_app/pages/intro_page.py b/packages/demo-app/demo_app/pages/intro_page.py new file mode 100644 index 000000000..965ea0f41 --- /dev/null +++ b/packages/demo-app/demo_app/pages/intro_page.py @@ -0,0 +1,49 @@ +from dara.components import Card, Icon, Spacer, Stack, Text +from dara.core.css import get_icon +from dara.core.visual.themes.light import Light + + +def italic_text(text: str): + return Text( + text, + font_size='22px', + padding='0px', + raw_css={'font-style': 'italic'}, + ) + + +def intro_page(): + return Stack( + Stack( + Card( + Stack( + Stack( + Icon(icon=get_icon('quote-left', size='2x'), color=Light.colors.text), + height='12px', + justify='center', + align='start', + ), + Spacer(), + italic_text('A framework called Dara has taken the stage'), + italic_text('Helping data scientists their users engage.'), + italic_text('Their graphs come alive, in colors so bright,'), + italic_text('Turning data to stories, from morning till night.'), + Spacer(), + Stack( + Icon(icon=get_icon('quote-right', size='2x'), color=Light.colors.text), + height='12px', + justify='center', + align='end', + ), + align='center', + justify='center', + ), + accent=True, + ), + height='60%', + width='70%', + # hug=True + ), + justify='center', + align='center', + ) diff --git a/packages/demo-app/demo_app/utils/components.py b/packages/demo-app/demo_app/utils/components.py new file mode 100644 index 000000000..9fa10f98a --- /dev/null +++ b/packages/demo-app/demo_app/utils/components.py @@ -0,0 +1,2280 @@ +import math + +import matplotlib.tri as tri +import numpy +import plotly.express as px +import plotly.graph_objects as go +import seaborn as sns +from bokeh.plotting import figure +from cai_causal_graph import CausalGraph +from matplotlib.figure import Figure +from pandas import DataFrame +from scipy.integrate import odeint + +from dara.components import ( + Accordion, + AccordionItem, + Anchor, + Bokeh, + BulletList, + Button, + ButtonBar, + ButtonStyle, + Card, + Carousel, + CarouselItem, + CausalGraphViewer, + CheckboxGroup, + Code, + ComponentSelectList, + Datepicker, + Form, + FormPage, + Grid, + Heading, + HtmlRaw, + Icon, + Image, + Input, + Item, + ItemBadge, + Label, + Markdown, + Matplotlib, + Modal, + NodeHierarchyBuilder, + Overlay, + Paragraph, + Plotly, + ProgressBar, + RadioGroup, + Select, + Slider, + Spacer, + Stack, + Switch, + Tab, + TabbedCard, + Table, + Text, + Textarea, + Tooltip, + VisualEdgeEncoder, +) +from dara.components.common.component_select_list import ComponentItem +from dara.components.graphs.components.edge_encoder import ( + EdgeConstraint, + EdgeConstraintType, +) +from dara.components.graphs.graph_layout import PlanarLayout, SpringLayout +from dara.components.plotting.palettes import PolarisingLight11 +from dara.core import ComponentInstance, DataVariable, Variable, py_component +from dara.core.css import get_icon +from dara.core.visual.themes.light import Light + +form_value = Variable({}) +show_modal = Variable(False) +show_overlay = Variable(True) +rate_value = Variable(7) +data = DataVariable( + DataFrame( + [ + { + 'col1': 'a', + 'col2': 1, + 'col3': 'F', + 'col4': '1990-02-12T00:00:00.000Z', + }, + { + 'col1': 'b', + 'col2': 2, + 'col3': 'M', + 'col4': '1991-02-12T00:00:00.000Z', + }, + { + 'col1': 'c', + 'col2': 3, + 'col3': 'M', + 'col4': '1991-02-12T00:00:00.000Z', + }, + { + 'col1': 'd', + 'col2': 4, + 'col3': 'F', + 'col4': '1994-02-07T00:00:00.000Z', + }, + { + 'col1': 'abc', + 'col2': 4, + 'col3': 'M', + 'col4': '1993-12-12T00:00:00.000Z', + }, + ] + ) +) + + +@py_component +def show_var(text: str, value): + return Stack(Text(text, bold=True), Text(str(value)), direction='horizontal') + + +def show_code(variable: Variable, code: str, component_type: str, component_name: str) -> ComponentInstance: + return Stack( + Button('Show Source Code', onclick=variable.update(value=True), width='200px', styling=ButtonStyle.GHOST), + Modal( + Stack(Code(code=code, theme=Code.Themes.LIGHT), scroll=True), + Stack( + Button( + 'Close', + onclick=variable.update(value=False), + width='200px', + styling=ButtonStyle.ERROR, + ), + align='end', + justify='end', + hug=True, + ), + show=variable, + height='700px', + width='1000px', + raw_css={'overflow-x': 'auto'}, + ), + Anchor( + 'Check the docs for more info', + href=f'https://dara.causalens.com/docs/generated/dara/reference/dara/components/{component_type}/{component_name}/', + new_tab=True, + ), + height='auto', + ) + + +# -------------------------------------------------------------------------------- +# Graph Components +# -------------------------------------------------------------------------------- + +causal_graph_var = Variable(False) + + +def causal_graph_viewer() -> ComponentInstance: + + causal_graph = CausalGraph() + causal_graph.add_edge('Age', 'Fraud') + causal_graph.add_edge('Authority Contacted', 'Fraud') + causal_graph.add_edge('CPI', 'Salary') + causal_graph.add_edge('Car Value', 'Fraud') + causal_graph.add_edge('Crime Rate', 'Fraud') + causal_graph.add_edge('Education Level', 'Fraud') + causal_graph.add_edge('Education Level', 'Occupation') + causal_graph.add_edge('Education Level', 'Salary') + causal_graph.add_edge('Location', 'Crime Rate') + causal_graph.add_edge('Marital Status', 'Fraud') + causal_graph.add_edge('Occupation', 'Salary') + causal_graph.add_edge('Previous Claims', 'Fraud') + causal_graph.add_edge('Salary', 'Car Value') + causal_graph.add_edge('Salary', 'Fraud') + causal_graph.add_edge('Total Claim', 'Fraud') + causal_graph.add_edge('Unemployment Rate', 'Salary') + + for node in causal_graph.nodes: + node.meta.update({'rendering_properties': {'color': numpy.random.choice(PolarisingLight11)}}) + + return Stack( + CausalGraphViewer( + causal_graph=causal_graph, + ), + show_code( + causal_graph_var, + """ +causal_graph = CausalGraph() +causal_graph.add_edge('Age', 'Fraud') +causal_graph.add_edge('Authority Contacted','Fraud') +causal_graph.add_edge('CPI','Salary') +causal_graph.add_edge('Car Value','Fraud') +causal_graph.add_edge('Crime Rate','Fraud') +causal_graph.add_edge('Education Level','Fraud') +causal_graph.add_edge('Education Level','Occupation') +causal_graph.add_edge('Education Level','Salary') +causal_graph.add_edge('Location','Crime Rate') +causal_graph.add_edge('Marital Status','Fraud') +causal_graph.add_edge('Occupation','Salary') +causal_graph.add_edge('Salary','Car Value') +causal_graph.add_edge('Salary','Fraud') +causal_graph.add_edge('Total Claim','Fraud') +causal_graph.add_edge('Unemployment Rate','Salary') + + +for node in causal_graph.nodes: + node.meta.update({'rendering_properties': {'color': numpy.random.choice(PolarisingLight11)}}) + +CausalGraphViewer( + causal_graph=causal_graph, +) + """, + 'graphs/components', + 'causal_graph_viewer', + ), + height='700px', + ) + + +def causal_graph_viewer_planar() -> ComponentInstance: + + causal_graph = CausalGraph() + causal_graph.add_edge('Age', 'Fraud') + causal_graph.add_edge('Authority Contacted', 'Fraud') + causal_graph.add_edge('CPI', 'Salary') + causal_graph.add_edge('Car Value', 'Fraud') + causal_graph.add_edge('Crime Rate', 'Fraud') + causal_graph.add_edge('Education Level', 'Fraud') + causal_graph.add_edge('Education Level', 'Occupation') + causal_graph.add_edge('Education Level', 'Salary') + causal_graph.add_edge('Location', 'Crime Rate') + causal_graph.add_edge('Marital Status', 'Fraud') + causal_graph.add_edge('Occupation', 'Salary') + causal_graph.add_edge('Previous Claims', 'Fraud') + causal_graph.add_edge('Salary', 'Car Value') + causal_graph.add_edge('Salary', 'Fraud') + causal_graph.add_edge('Total Claim', 'Fraud') + causal_graph.add_edge('Unemployment Rate', 'Salary') + + return Stack( + CausalGraphViewer( + causal_graph=causal_graph, + graph_layout=PlanarLayout(), + ), + show_code( + causal_graph_var, + """ +causal_graph = CausalGraph() +causal_graph.add_edge('Age', 'Fraud') +causal_graph.add_edge('Authority Contacted','Fraud') +causal_graph.add_edge('CPI','Salary') +causal_graph.add_edge('Car Value','Fraud') +causal_graph.add_edge('Crime Rate','Fraud') +causal_graph.add_edge('Education Level','Fraud') +causal_graph.add_edge('Education Level','Occupation') +causal_graph.add_edge('Education Level','Salary') +causal_graph.add_edge('Location','Crime Rate') +causal_graph.add_edge('Marital Status','Fraud') +causal_graph.add_edge('Occupation','Salary') +causal_graph.add_edge('Salary','Car Value') +causal_graph.add_edge('Salary','Fraud') +causal_graph.add_edge('Total Claim','Fraud') +causal_graph.add_edge('Unemployment Rate','Salary') + + +for node in causal_graph.nodes: + node.meta.update({'rendering_properties': {'color': numpy.random.choice(PolarisingLight11)}}) + +CausalGraphViewer( + causal_graph=causal_graph, + graph_layout=PlanarLayout(), +) + """, + 'graphs/components', + 'causal_graph_viewer', + ), + height='700px', + ) + + +edge_encoder_var = Variable(False) + + +def edge_encoder() -> ComponentInstance: + return Stack( + VisualEdgeEncoder( + nodes=['Age', 'Unemployment', 'Education', 'Income'], + initial_constraints=[ + EdgeConstraint(source='Age', target='Education', type=EdgeConstraintType.FORBIDDEN_EDGE), + EdgeConstraint(source='Unemployment', target='Education', type=EdgeConstraintType.HARD_UNDIRECTED_EDGE), + EdgeConstraint(source='Unemployment', target='Income', type=EdgeConstraintType.HARD_DIRECTED_EDGE), + EdgeConstraint(source='Education', target='Income', type=EdgeConstraintType.HARD_DIRECTED_EDGE), + ], + graph_layout=SpringLayout(), + ), + show_code( + edge_encoder_var, + """ +VisualEdgeEncoder( + nodes=['Age', 'Unemployment', 'Education', 'Income'], + initial_constraints=[ + EdgeConstraint(source='Age', target='Education', type=EdgeConstraintType.FORBIDDEN_EDGE), + EdgeConstraint(source='Unemployment', target='Education', type=EdgeConstraintType.HARD_UNDIRECTED_EDGE), + EdgeConstraint(source='Unemployment', target='Income', type=EdgeConstraintType.HARD_DIRECTED_EDGE), + EdgeConstraint(source='Education', target='Income', type=EdgeConstraintType.HARD_DIRECTED_EDGE) + ], + graph_layout=SpringLayout(), +) + """, + 'graphs/components', + 'visual_edge_encoder', + ), + height='500px', + ) + + +node_hierarchy_var = Variable(False) + + +def node_hierarchy_builder() -> ComponentInstance: + return Stack( + NodeHierarchyBuilder( + nodes=[['Age'], ['Unemployment', 'Education'], ['Income']], + ), + show_code( + node_hierarchy_var, + """ +NodeHierarchyBuilder( + nodes=[['Age'], ['Unemployment', 'Education'], ['Income']], + ) + """, + 'graphs/components', + 'node_hierarchy_builder', + ), + height='500px', + ) + + +# -------------------------------------------------------------------------------- +# Plotting Components +# -------------------------------------------------------------------------------- + +bokeh_var = Variable(False) + + +def bokeh() -> ComponentInstance: + def get_bokeh_figure(): + + sigma = 10 + rho = 28 + beta = 8.0 / 3 + theta = 3 * numpy.pi / 4 + + def lorenz(xyz, t): + x, y, z = xyz + x_dot = sigma * (y - x) + y_dot = x * rho - x * z - y + z_dot = x * y - beta * z + return [x_dot, y_dot, z_dot] + + initial = (-10, -7, 35) + t = numpy.arange(0, 100, 0.006) + + solution = odeint(lorenz, initial, t) + + x = solution[:, 0] + y = solution[:, 1] + z = solution[:, 2] + xprime = numpy.cos(theta) * x - numpy.sin(theta) * y + + colors = ['#C6DBEF', '#9ECAE1', '#6BAED6', '#4292C6', '#2171B5', '#08519C', '#08306B'] + + p = figure(title='Lorenz attractor example', background_fill_color='#fafafa') + + p.multi_line( + numpy.array_split(xprime, 7), numpy.array_split(z, 7), line_color=colors, line_alpha=0.8, line_width=1.5 + ) + + return p + + return Stack( + Bokeh(get_bokeh_figure()), + show_code( + bokeh_var, + """ +def get_bokeh_figure(): + + sigma = 10 + rho = 28 + beta = 8.0/3 + theta = 3 * numpy.pi / 4 + + def lorenz(xyz, t): + x, y, z = xyz + x_dot = sigma * (y - x) + y_dot = x * rho - x * z - y + z_dot = x * y - beta* z + return [x_dot, y_dot, z_dot] + + initial = (-10, -7, 35) + t = numpy.arange(0, 100, 0.006) + + solution = odeint(lorenz, initial, t) + + x = solution[:, 0] + y = solution[:, 1] + z = solution[:, 2] + xprime = numpy.cos(theta) * x - numpy.sin(theta) * y + + colors = ["#C6DBEF", "#9ECAE1", "#6BAED6", "#4292C6", "#2171B5", "#08519C", "#08306B"] + + p = figure(title="Lorenz attractor example", background_fill_color="#fafafa") + + p.multi_line(numpy.array_split(xprime, 7), numpy.array_split(z, 7), + line_color=colors, line_alpha=0.8, line_width=1.5) + + return p + +Bokeh(get_bokeh_figure()) + """, + 'plotting', + 'bokeh', + ), + ) + + +plotly_var = Variable(False) + + +def plotly() -> ComponentInstance: + def get_plotly_figure(): + + # Load data, define hover text and bubble size + data = px.data.gapminder() + df_2007 = data[data['year'] == 2007] + df_2007 = df_2007.sort_values(['continent', 'country']) + + hover_text = [] + bubble_size = [] + + for index, row in df_2007.iterrows(): + hover_text.append( + ( + 'Country: {country}
' + + 'Life Expectancy: {lifeExp}
' + + 'GDP per capita: {gdp}
' + + 'Population: {pop}
' + + 'Year: {year}' + ).format( + country=row['country'], + lifeExp=row['lifeExp'], + gdp=row['gdpPercap'], + pop=row['pop'], + year=row['year'], + ) + ) + bubble_size.append(math.sqrt(row['pop'])) + + df_2007['text'] = hover_text + df_2007['size'] = bubble_size + sizeref = 2.0 * max(df_2007['size']) / (100**2) + + # Dictionary with dataframes for each continent + continent_names = ['Africa', 'Americas', 'Asia', 'Europe', 'Oceania'] + continent_data = {continent: df_2007.query("continent == '%s'" % continent) for continent in continent_names} + + # Create figure + fig = go.Figure() + + for continent_name, continent in continent_data.items(): + fig.add_trace( + go.Scatter( + x=continent['gdpPercap'], + y=continent['lifeExp'], + name=continent_name, + text=continent['text'], + marker_size=continent['size'], + ) + ) + + # Tune marker appearance and layout + fig.update_traces(mode='markers', marker=dict(sizemode='area', sizeref=sizeref, line_width=2)) + + fig.update_layout( + title='Life Expectancy v. Per Capita GDP, 2007', + xaxis=dict( + title='GDP per capita (2000 dollars)', + gridcolor='white', + type='log', + gridwidth=2, + ), + yaxis=dict( + title='Life Expectancy (years)', + gridcolor='white', + gridwidth=2, + ), + paper_bgcolor='rgb(243, 243, 243)', + plot_bgcolor='rgb(243, 243, 243)', + ) + + return fig + + return Stack( + Plotly(get_plotly_figure()), + show_code( + plotly_var, + """ +def get_plotly_figure(): + # Load data, define hover text and bubble size + data = px.data.gapminder() + df_2007 = data[data['year']==2007] + df_2007 = df_2007.sort_values(['continent', 'country']) + + hover_text = [] + bubble_size = [] + + for index, row in df_2007.iterrows(): + hover_text.append(('Country: {country}
'+ + 'Life Expectancy: {lifeExp}
'+ + 'GDP per capita: {gdp}
'+ + 'Population: {pop}
'+ + 'Year: {year}').format(country=row['country'], + lifeExp=row['lifeExp'], + gdp=row['gdpPercap'], + pop=row['pop'], + year=row['year'])) + bubble_size.append(math.sqrt(row['pop'])) + + df_2007['text'] = hover_text + df_2007['size'] = bubble_size + sizeref = 2.*max(df_2007['size'])/(100**2) + + # Dictionary with dataframes for each continent + continent_names = ['Africa', 'Americas', 'Asia', 'Europe', 'Oceania'] + continent_data = {continent:df_2007.query("continent == '%s'" %continent) + for continent in continent_names} + + # Create figure + fig = go.Figure() + + for continent_name, continent in continent_data.items(): + fig.add_trace(go.Scatter( + x=continent['gdpPercap'], y=continent['lifeExp'], + name=continent_name, text=continent['text'], + marker_size=continent['size'], + )) + + # Tune marker appearance and layout + fig.update_traces(mode='markers', marker=dict(sizemode='area', + sizeref=sizeref, line_width=2)) + + fig.update_layout( + title='Life Expectancy v. Per Capita GDP, 2007', + xaxis=dict( + title='GDP per capita (2000 dollars)', + gridcolor='white', + type='log', + gridwidth=2, + ), + yaxis=dict( + title='Life Expectancy (years)', + gridcolor='white', + gridwidth=2, + ), + paper_bgcolor='rgb(243, 243, 243)', + plot_bgcolor='rgb(243, 243, 243)', + ) + + return fig + +Plotly(get_plotly_figure()) + """, + 'plotting', + 'plotly', + ), + height='500px', + ) + + +matplotlib_var = Variable(False) + + +def matplotlib() -> ComponentInstance: + def get_matplotlib_figure(): + # see full example here: https://matplotlib.org/stable/gallery/images_contours_and_fields/irregulardatagrid.html#sphx-glr-gallery-images-contours-and-fields-irregulardatagrid-py + + # Create a Figure object + fig = Figure(figsize=(8, 6)) + + # Generate some sample data + npts = 200 + ngridx = 100 + ngridy = 200 + x = numpy.random.uniform(-2, 2, npts) + y = numpy.random.uniform(-2, 2, npts) + z = x * numpy.exp(-(x**2) - y**2) + + # Add a subplot to the Figure + ax1 = fig.add_subplot() + ax2 = fig.add_subplot() + + # Create grid values first. + xi = numpy.linspace(-2.1, 2.1, ngridx) + yi = numpy.linspace(-2.1, 2.1, ngridy) + + # Linearly interpolate the data (x, y) on a grid defined by (xi, yi). + triang = tri.Triangulation(x, y) + interpolator = tri.LinearTriInterpolator(triang, z) + Xi, Yi = numpy.meshgrid(xi, yi) + zi = interpolator(Xi, Yi) + + ax1.contour(xi, yi, zi, levels=14, linewidths=0.5, colors='k') + cntr1 = ax1.contourf(xi, yi, zi, levels=14, cmap='RdBu_r') + + fig.colorbar(cntr1, ax=ax1) + ax1.plot(x, y, 'ko', ms=3) + ax1.set(xlim=(-2, 2), ylim=(-2, 2)) + + # ---------- + # Tricontour + # ---------- + # Directly supply the unordered, irregularly spaced coordinates + # to tricontour. + + ax2.tricontour(x, y, z, levels=14, linewidths=0.5, colors='k') + cntr2 = ax2.tricontourf(x, y, z, levels=14, cmap='RdBu_r') + + fig.colorbar(cntr2, ax=ax2) + ax2.plot(x, y, 'ko', ms=3) + ax2.set(xlim=(-2, 2), ylim=(-2, 2)) + return fig + + return Stack( + Matplotlib(get_matplotlib_figure()), + show_code( + matplotlib_var, + """ +def get_matplotlib_figure(): + # see full example here: https://matplotlib.org/stable/gallery/images_contours_and_fields/irregulardatagrid.html#sphx-glr-gallery-images-contours-and-fields-irregulardatagrid-py + # Create a Figure object + fig = Figure(figsize=(8, 6)) + + # Generate some sample data + npts = 200 + ngridx = 100 + ngridy = 200 + x = numpy.random.uniform(-2, 2, npts) + y = numpy.random.uniform(-2, 2, npts) + z = x * numpy.exp(-x**2 - y**2) + + # Add a subplot to the Figure + ax1, ax2 = fig.add_subplot(nrows=2) + + # Create grid values first. + xi = numpy.linspace(-2.1, 2.1, ngridx) + yi = numpy.linspace(-2.1, 2.1, ngridy) + + # Linearly interpolate the data (x, y) on a grid defined by (xi, yi). + triang = tri.Triangulation(x, y) + interpolator = tri.LinearTriInterpolator(triang, z) + Xi, Yi = numpy.meshgrid(xi, yi) + zi = interpolator(Xi, Yi) + + # Note that scipy.interpolate provides means to interpolate data on a grid + # as well. The following would be an alternative to the four lines above: + # from scipy.interpolate import griddata + # zi = griddata((x, y), z, (xi[None, :], yi[:, None]), method='linear') + + ax1.contour(xi, yi, zi, levels=14, linewidths=0.5, colors='k') + cntr1 = ax1.contourf(xi, yi, zi, levels=14, cmap="RdBu_r") + + fig.colorbar(cntr1, ax=ax1) + ax1.plot(x, y, 'ko', ms=3) + ax1.set(xlim=(-2, 2), ylim=(-2, 2)) + ax1.set_title('grid and contour (%d points, %d grid points)' %(npts, ngridx * ngridy)) + + # ---------- + # Tricontour + # ---------- + # Directly supply the unordered, irregularly spaced coordinates + # to tricontour. + + ax2.tricontour(x, y, z, levels=14, linewidths=0.5, colors='k') + cntr2 = ax2.tricontourf(x, y, z, levels=14, cmap="RdBu_r") + + fig.colorbar(cntr2, ax=ax2) + ax2.plot(x, y, 'ko', ms=3) + ax2.set(xlim=(-2, 2), ylim=(-2, 2)) + ax2.set_title('tricontour (%d points)' % npts) + return fig + +Matplotlib(get_matplotlib_figure()), + """, + 'plotting', + 'matplotlib', + ), + ) + + +seaborn_var = Variable(False) + + +def seaborn() -> ComponentInstance: + def get_seaborn_figure(): + # Create a Figure object + fig = Figure(figsize=(8, 6)) + + # Generate some sample data + tips = sns.load_dataset('tips') + + # Add a subplot to the Figure + ax = fig.add_subplot() + + # Create a scatter plot using Seaborn + sns.scatterplot(data=tips, x='total_bill', y='tip', hue='time', style='time', ax=ax) + + # Customize the plot as needed + ax.set_xlabel('Total Bill') + ax.set_ylabel('Tip') + ax.set_title('Scatter Plot with Figure') + + return fig + + return Stack( + Matplotlib(get_seaborn_figure()), + show_code( + matplotlib_var, + """ +def get_seaborn_figure(): + # Create a Figure object + fig = Figure(figsize=(8, 6)) + + # Generate some sample data + tips = sns.load_dataset('tips') + + # Add a subplot to the Figure + ax = fig.add_subplot() + + # Create a scatter plot using Seaborn + sns.scatterplot(data=tips, x='total_bill', y='tip', hue='time', style='time', ax=ax) + + # Customize the plot as needed + ax.set_xlabel('Total Bill') + ax.set_ylabel('Tip') + ax.set_title('Scatter Plot with Figure') + +Matplotlib(get_seaborn_figure()), + """, + 'plotting', + 'matplotlib', + ), + ) + + +# -------------------------------------------------------------------------------- +# Common Components +# -------------------------------------------------------------------------------- + +accordion_var = Variable(False) + + +def accordion() -> ComponentInstance: + return Stack( + Accordion( + items=[ + AccordionItem( + label='First item', + content=Text('This is some content'), + badge=ItemBadge(label='Label', color=Light.colors.violet), + ), + AccordionItem( + label='Second item', + content=Text('This is some content'), + badge=ItemBadge(label='Label', color=Light.colors.teal), + ), + AccordionItem( + label='Third item', + content=Text('This is some content'), + badge=ItemBadge(label='Label', color=Light.colors.orange), + ), + ], + ), + show_code( + accordion_var, + """ +Accordion( + items=[ + AccordionItem( + label='First item', + content=Text('This is some content'), + badge=ItemBadge(label='Label', color=Light.colors.violet), + ), + AccordionItem( + label='Second item', + content=Text('This is some content'), + badge=ItemBadge(label='Label', color=Light.colors.teal), + ), + AccordionItem( + label='Third item', + content=Text('This is some content'), + badge=ItemBadge(label='Label', color=Light.colors.orange), + ), + ], +) + """, + 'common', + 'accordion', + ), + ) + + +anchor_var = Variable(False) + + +def anchor() -> ComponentInstance: + return Stack( + Anchor('Link to causaLens website', href='https://www.causalens.com/', new_tab=True), + show_code( + anchor_var, + """ +Anchor('Link to causaLens website', href='https://www.causalens.com/', new_tab=True) + """, + 'common', + 'anchor', + ), + ) + + +button_var = Variable() + + +def button() -> ComponentInstance: + return Stack( + Text('Default Styles:'), + Stack( + Button('Primary', width='180px'), + Button('Secondary', styling=ButtonStyle.SECONDARY, width='180px'), + Button('Error', styling=ButtonStyle.ERROR, width='180px'), + direction='horizontal', + height='3rem', + ), + Text('Outline Styles:'), + Stack( + Button('Primary Outline', outline=True, width='180px'), + Button('Secondary Outline', styling=ButtonStyle.SECONDARY, outline=True, width='180px'), + Button('Error Outline', styling=ButtonStyle.ERROR, outline=True, width='180px'), + direction='horizontal', + height='3rem', + ), + Text('Custom Component:'), + Button( + Stack( + Text( + 'This is an example of a Button with a Stack as its child', + ), + ), + width='564px', + outline=True, + ), + show_code( + button_var, + """ +Stack( + Text('Default Styles:'), + Stack( + Button('Primary', width='180px'), + Button('Secondary', styling=ButtonStyle.SECONDARY, width='180px'), + Button('Error', styling=ButtonStyle.ERROR, width='180px'), + direction='horizontal', + height='3rem', + ), + Text('Outline Styles:'), + Stack( + Button('Primary Outline', outline=True, width='180px'), + Button('Secondary Outline', styling=ButtonStyle.SECONDARY, outline=True, width='180px'), + Button('Error Outline', styling=ButtonStyle.ERROR, outline=True, width='180px'), + direction='horizontal', + height='3rem', + ), + Text('Custom Component:'), + Button( + Stack( + Text( + 'This is an example of a Button with a Stack as its child', + ), + ), + width='564px', + outline=True, + ), +) + """, + 'common', + 'button', + ), + ) + + +bullet_list_var = Variable() + + +def bullet_list() -> ComponentInstance: + return Stack( + Stack( + Stack( + Text('Ordered BulletList:'), + BulletList(items=['My', 'ordered', 'Bullet', 'List'], numbered=True), + ), + Stack( + Text('Unordered BulletList:'), + BulletList(items=['My', 'Unordered', 'Bullet', 'List']), + ), + direction='horizontal', + ), + show_code( + bullet_list_var, + """ +Stack( + Stack( + Text('Ordered BulletList:'), + BulletList(items=['My', 'ordered', 'Bullet', 'List'], numbered=True), + ), + Stack( + Text('Unordered BulletList:'), + BulletList(items=['My', 'Unordered', 'Bullet', 'List']), + ), + direction='horizontal', +) + """, + 'common', + 'bullet_list', + ), + ) + + +button_bar_var = Variable(False) + + +def button_bar() -> ComponentInstance: + return Stack( + Text('Primary Style:'), + ButtonBar( + items=[ + Item(label='Value 1', value='val1'), + Item(label='Value 2', value='val2'), + Item(label='Value 3', value='val3'), + ], + height='2.5rem', + ), + Text('Secondary Style:'), + ButtonBar( + items=[ + Item(label='Value 1', value='val1'), + Item(label='Value 2', value='val2'), + Item(label='Value 3', value='val3'), + ], + styling='secondary', + height='2.5rem', + ), + show_code( + button_bar_var, + """ +Stack( + Text('Primary Style:'), + ButtonBar( + items=[ + Item(label='Value 1', value='val1'), + Item(label='Value 2', value='val2'), + Item(label='Value 3', value='val3'), + ], + height='2.5rem', + ), + Text('Secondary Style:'), + ButtonBar( + items=[ + Item(label='Value 1', value='val1'), + Item(label='Value 2', value='val2'), + Item(label='Value 3', value='val3'), + ], + styling='secondary', + height='2.5rem', + ), +) + """, + 'common', + 'button_bar', + ), + ) + + +card_var = Variable(False) + + +def card() -> ComponentInstance: + return Stack( + Stack( + Card( + Stack( + Text('Some content'), + ), + title='Title', + subtitle='subtitle', + ), + Card( + Stack( + Text('Some content'), + ), + title='Title', + subtitle='subtitle', + accent=True, + ), + direction='horizontal', + align='start', + ), + show_code( + card_var, + """ +Stack( + Card( + Stack( + Text('Some content'), + ), + title='Title', + subtitle='subtitle', + ), + Card( + Stack( + Text('Some content'), + ), + title='Title', + subtitle='subtitle', + accent=True, + ), + direction='horizontal', + align='start', +) + """, + 'common', + 'card', + ), + ) + + +carousel_var = Variable(False) + + +def carousel() -> ComponentInstance: + return Stack( + Carousel( + items=[ + CarouselItem( + title='Dog', + subtitle='Image of a good boy getting his biscuit', + image='https://www.preventivevet.com/hs-fs/hubfs/pug%20treats.jpg?width=600&height=300&name=pug%20treats.jpg', + ), + CarouselItem( + title='Cat', + subtitle='Image of a cat staring into oblivion', + image='https://moderncat.com/wp-content/uploads/2021/01/bigstock-Domestic-Cat-Beautiful-Old-Ca-353858042.png', + component=Button('Component Example'), + ), + ] + ), + show_code( + carousel_var, + """ +rate_value = Variable(7) + +@py_component +def rate_image(value: int): + if value < 2: + return Icon(icon=get_icon('face-sad-tear', size='lg')) + if value < 4: + return Icon(icon=get_icon('face-frown', size='lg')) + if value < 6: + return Icon(icon=get_icon('face-meh', size='lg')) + if value <= 8: + return Icon(icon=get_icon('face-smile', size='lg')) + return Icon(icon=get_icon('face-laugh-beam', size='lg')) + +Carousel( + items=[ + CarouselItem( + title='Dog', + subtitle='Image of a good boy getting his biscuit', + image='https://www.preventivevet.com/hs-fs/hubfs/pug%20treats.jpg?width=600&height=300&name=pug%20treats.jpg', + component=Stack( + Label( + Slider(domain=[0, 10], value=rate_value, disable_input_alternative=True), + value='Rate this image:', + direction='horizontal', + bold=True, + width='90%', + ), + rate_image(rate_value), + direction='horizontal', + ), + ), + CarouselItem( + title='Cat', + subtitle='Image of a cat staring into oblivion', + image='https://moderncat.com/wp-content/uploads/2021/01/bigstock-Domestic-Cat-Beautiful-Old-Ca-353858042.png', + component=Button('Component Example'), + ), + ] +), + """, + 'common', + 'carousel', + ), + ) + + +checkbox_group_var = Variable(False) + + +def checkbox_group() -> ComponentInstance: + return Stack( + CheckboxGroup( + select_max=2, + items=['first', 'second', 'third', 'fourth', 'fifth'], + ), + show_code( + checkbox_group_var, + """ +CheckboxGroup( + select_max=2, + items=['first', 'second', 'third', 'fourth', 'fifth'], +) + """, + 'common', + 'checkbox_group', + ), + ) + + +code_var = Variable(False) + + +def code() -> ComponentInstance: + return Stack( + Stack( + Label( + Code(code='def some_func():\n pass', theme=Code.Themes.LIGHT, width='300px', height='100px'), + value='Light Mode:', + ), + Label( + Code(code='def some_func():\n pass', theme=Code.Themes.DARK, width='300px', height='100px'), + value='Dark Mode:', + ), + direction='horizontal', + align='start', + justify='space-evenly', + ), + show_code( + code_var, + """ +Stack( + Label( + Code(code='def some_func():\n pass', theme=Code.Themes.LIGHT, width='300px', height='100px'), + value='Light Mode:', + ), + Label( + Code(code='def some_func():\n pass', theme=Code.Themes.DARK, width='300px', height='100px'), + value='Dark Mode:', + ), + direction='horizontal', + align='start', + justify='space-evenly', +) + """, + 'common', + 'code', + ), + ) + + +component_select_list_var = Variable(False) + + +def component_select_list() -> ComponentInstance: + return Stack( + ComponentSelectList( + items=[ + ComponentItem(title='TitleA', subtitle='subtitle', component=Text('A')), + ComponentItem(title='TitleB', subtitle='subtitle', component=Text('B')), + ComponentItem(title='TitleC', subtitle='subtitle', component=Text('C')), + ], + selected_items=Variable('TitleA'), + ), + show_code( + component_select_list_var, + """ +ComponentSelectList( + items=[ + ComponentItem(title='TitleA', subtitle='subtitle', component=Text('A')), + ComponentItem(title='TitleB', subtitle='subtitle', component=Text('B')), + ComponentItem(title='TitleC', subtitle='subtitle', component=Text('C')), + ], + selected_items=Variable('TitleA'), +) + """, + 'common', + 'component_select_list', + ), + ) + + +datepicker_var = Variable(False) + + +def datepicker() -> ComponentInstance: + return Stack( + Label(Datepicker(), value='Plain datepicker:', width='fit-content'), + Label(Datepicker(enable_time=True), value='Datepicker with Time:', width='fit-content'), + Label(Datepicker(range=True), value='Datepicker with Range:', width='fit-content'), + Label(Datepicker(enable_time=True, range=True), value='Datepicker with Time and Range:', width='fit-content'), + show_code( + datepicker_var, + """ +Stack( + Label(Datepicker(), value='Plain datepicker:', width='fit-content'), + Label(Datepicker(enable_time=True), value='Datepicker with Time:', width='fit-content'), + Label(Datepicker(range=True), value='Datepicker with Range:', width='fit-content'), + Label(Datepicker(enable_time=True, range=True), value='Datepicker with Time and Range:', width='fit-content'), +) + """, + 'common', + 'datepicker', + ), + ) + + +form_var = Variable(False) + + +def form() -> ComponentInstance: + return Stack( + Form( + FormPage( + Label( + Input( + id='name', + raw_css="""input { + width: 19rem; + }""", + ), + value='Name:', + direction='horizontal', + label_width='20%', + bold=True, + ), + Label( + Datepicker(id='lifespan', range=True), + value='Lifespan:', + direction='horizontal', + label_width='20%', + bold=True, + ), + Label( + Select( + items=[ + Item(label='dog', value=1), + Item(label='cat', value=2), + Item(label='parrot', value=3), + Item(label='rat', value=4), + Item(label='rabbit', value=5), + ], + id='favourite_pet', + multiselect=True, + width='19rem', + ), + value='Favourite pets:', + direction='horizontal', + label_width='20%', + bold=True, + ), + height='200px', + ), + FormPage( + Stack( + Stack( + Label( + Slider( + domain=[0, 100], + ticks=[0, 20, 40, 60, 80, 100], + id='employee_number', + disable_input_alternative=True, + width='70%', + ), + value='Employee Number:', + direction='horizontal', + label_width='40%', + bold=True, + ), + Label( + Slider( + domain=[0, 100], + ticks=[0, 20, 40, 60, 80, 100], + id='company_culture', + disable_input_alternative=True, + width='70%', + ), + value='Company Culture:', + direction='horizontal', + label_width='40%', + bold=True, + ), + ), + Stack( + Label( + Slider( + domain=[0, 100], + ticks=[0, 20, 40, 60, 80, 100], + id='company_benefits', + disable_input_alternative=True, + width='70%', + ), + value='Company Benefits:', + direction='horizontal', + label_width='40%', + bold=True, + ), + Label( + Slider( + domain=[0, 100], + ticks=[0, 20, 40, 60, 80, 100], + id='salary', + disable_input_alternative=True, + width='70%', + ), + value='Salary (R$):', + direction='horizontal', + label_width='40%', + bold=True, + ), + ), + direction='horizontal', + ), + height='200px', + ), + FormPage( + Label( + Input( + id='name2', + raw_css="""input { + width: 19rem; + }""", + ), + value='Dog Name:', + direction='horizontal', + label_width='20%', + bold=True, + ), + Label( + Datepicker(id='lifespan2', range=True), + value='Lifespan of a fly:', + direction='horizontal', + label_width='20%', + bold=True, + ), + Label( + Select( + items=[ + Item(label='red', value=1), + Item(label='blue', value=2), + Item(label='green', value=3), + ], + id='favourite_color', + multiselect=True, + width='19rem', + ), + value='Favourite color:', + direction='horizontal', + label_width='20%', + bold=True, + ), + height='200px', + ), + value=form_value, + onsubmit=form_value.update(value={}), + ), + show_var('Form Variable:', form_value), + show_code( + form_var, + ''' +Stack( + Form( + FormPage( + Label( + Input( + id='name', + raw_css="""input { + width: 19rem; + }""", + ), + value='Name:', + direction='horizontal', + label_width='20%', + bold=True, + ), + Label( + Datepicker(id='lifespan', range=True), + value='Lifespan:', + direction='horizontal', + label_width='20%', + bold=True, + ), + Label( + Select( + items=[ + Item(label='dog', value=1), + Item(label='cat', value=2), + Item(label='parrot', value=3), + Item(label='rat', value=4), + Item(label='rabbit', value=5), + ], + id='favourite_pet', + multiselect=True, + width='19rem', + ), + value='Favourite pets:', + direction='horizontal', + label_width='20%', + bold=True, + ), + height='200px', + ), + FormPage( + Stack( + Stack( + Label( + Slider( + domain=[0, 100], + ticks=[0, 20, 40, 60, 80, 100], + id='employee_number', + disable_input_alternative=True, + width='70%', + ), + value='Employee Number:', + direction='horizontal', + label_width='40%', + bold=True, + ), + Label( + Slider( + domain=[0, 100], + ticks=[0, 20, 40, 60, 80, 100], + id='company_culture', + disable_input_alternative=True, + width='70%', + ), + value='Company Culture:', + direction='horizontal', + label_width='40%', + bold=True, + ), + ), + Stack( + Label( + Slider( + domain=[0, 100], + ticks=[0, 20, 40, 60, 80, 100], + id='company_benefits', + disable_input_alternative=True, + width='70%', + ), + value='Company Benefits:', + direction='horizontal', + label_width='40%', + bold=True, + ), + Label( + Slider( + domain=[0, 100], + ticks=[0, 20, 40, 60, 80, 100], + id='salary', + disable_input_alternative=True, + width='70%', + ), + value='Salary (R$):', + direction='horizontal', + label_width='40%', + bold=True, + ), + ), + direction='horizontal', + ), + height='200px', + ), + FormPage( + Label( + Input( + id='name2', + raw_css="""input { + width: 19rem; + }""", + ), + value='Dog Name:', + direction='horizontal', + label_width='20%', + bold=True, + ), + Label( + Datepicker(id='lifespan2', range=True), + value='Lifespan of a fly:', + direction='horizontal', + label_width='20%', + bold=True, + ), + Label( + Select( + items=[ + Item(label='red', value=1), + Item(label='blue', value=2), + Item(label='green', value=3), + ], + id='favourite_color', + multiselect=True, + width='19rem', + ), + value='Favourite color:', + direction='horizontal', + label_width='20%', + bold=True, + ), + height='200px', + ), + value=form_value, + onsubmit=form_value.update(value={}), + ), + show_var('Form Variable:', form_value), +) + ''', + 'common', + 'form', + ), + ) + + +grid_var = Variable(False) + + +def grid() -> ComponentInstance: + return Stack( + Grid( + Grid.Row( + Grid.Column(Text('Span = 2'), span=2, background='deepskyblue'), + Grid.Column(Text('Span = 2'), span=2, background='deepskyblue'), + Grid.Column(Text('Span = 2'), span=2, background='deepskyblue'), + Grid.Column(Text('Span = 2'), span=2, background='deepskyblue'), + Grid.Column(Text('Span = 2'), span=2, background='deepskyblue'), + Grid.Column(Text('Span = 2'), span=2, background='deepskyblue'), + column_gap=1, + ), + Grid.Row( + Grid.Column(Text('Span = 4'), span=4, background='dodgerblue'), + Grid.Column(Text('Span = 4'), span=4, background='dodgerblue'), + Grid.Column(Text('Span = 4'), span=4, background='dodgerblue'), + column_gap=1, + ), + Grid.Row( + Grid.Column(Text('Span = 6'), span=6, background='skyblue'), + Grid.Column(Text('Span = 6'), span=6, background='skyblue'), + column_gap=1, + ), + Grid.Row( + Grid.Column(Text('Span = 12'), span=12, background='steelblue'), + ), + ), + show_code( + grid_var, + """ +Grid( + Grid.Row( + Grid.Column(Text('Span = 2'), span=2, background='deepskyblue'), + Grid.Column(Text('Span = 2'), span=2, background='deepskyblue'), + Grid.Column(Text('Span = 2'), span=2, background='deepskyblue'), + Grid.Column(Text('Span = 2'), span=2, background='deepskyblue'), + Grid.Column(Text('Span = 2'), span=2, background='deepskyblue'), + Grid.Column(Text('Span = 2'), span=2, background='deepskyblue'), + column_gap=1, + ), + Grid.Row( + Grid.Column(Text('Span = 4'), span=4, background='dodgerblue'), + Grid.Column(Text('Span = 4'), span=4, background='dodgerblue'), + Grid.Column(Text('Span = 4'), span=4, background='dodgerblue'), + column_gap=1, + ), + Grid.Row( + Grid.Column(Text('Span = 6'), span=6, background='skyblue'), + Grid.Column(Text('Span = 6'), span=6, background='skyblue'), + column_gap=1, + ), + Grid.Row( + Grid.Column(Text('Span = 12'), span=12, background='steelblue'), + ), +) + """, + 'common', + 'grid', + ), + ) + + +heading_var = Variable(False) + + +def heading() -> ComponentInstance: + return Stack( + Heading('Heading 1', level=1), + Heading('Heading 2', level=2), + Heading('Heading 3', level=3), + show_code( + heading_var, + """ +Stack( + Heading('Heading 1', level=1), + Heading('Heading 2', level=2), + Heading('Heading 3', level=3), +) + """, + 'common', + 'heading', + ), + ) + + +html_raw_var = Variable(False) + + +def html_raw() -> ComponentInstance: + return Stack( + HtmlRaw( + html='', + raw_css={'min-height': '400px'}, + ), + show_code( + html_raw_var, + """ +HtmlRaw( + html='', + raw_css={'min-height': '400px'}, +) + """, + 'common', + 'html_raw', + ), + ) + + +icon_var = Variable(False) + + +def icon() -> ComponentInstance: + return Stack( + Icon( + icon=get_icon('spaghetti-monster-flying', size='10x'), + ), + show_code( + icon_var, + """ +Icon( + icon=get_icon('spaghetti-monster-flying', size='10x'), +) + """, + 'common', + 'icon', + ), + ) + + +image_var = Variable(False) + + +def image() -> ComponentInstance: + return Stack( + Image(src='https://i.natgeofe.com/n/f9e19f16-ecb4-4cd3-9fe9-83423ace1b1a/tree-goats-morocco.jpg'), + show_code( + image_var, + """ +Image(src='https://i.natgeofe.com/n/f9e19f16-ecb4-4cd3-9fe9-83423ace1b1a/tree-goats-morocco.jpg') + """, + 'common', + 'image', + ), + ) + + +input_var = Variable(False) + + +def input() -> ComponentInstance: + return Stack( + Label(Input(), value='Text Input:'), + Label(Input(type='number'), value='Numeric Input:'), + show_code( + input_var, + """ +Stack( + Label(Input(), value='Text Input:'), + Label(Input(type='number'), value='Numeric Input:'), +) + """, + 'common', + 'input', + ), + ) + + +label_var = Variable(False) + + +def label() -> ComponentInstance: + return Stack( + Label( + Input(), + value='Vertical Label:', + ), + Label( + Input(), + value='Horizontal Label:', + direction='horizontal', + ), + show_code( + label_var, + """ +Stack( + Label( + Input(), + value='Vertical Label:', + ), + Label( + Input(), + value='Horizontal Label:', + direction='horizontal', + ), +) + """, + 'common', + 'label', + ), + ) + + +markdown_var = Variable(False) + + +def markdown() -> ComponentInstance: + return Stack( + Markdown(' ## Heading\n ### Subheading\n Some other text'), + show_code( + markdown_var, + """ +Markdown(' ## Heading\\n ### Subheading\\n Some other text') + """, + 'common', + 'markdown', + ), + ) + + +modal_var = Variable(False) + + +def modal() -> ComponentInstance: + return Stack( + Button('Click to show modal', onclick=show_modal.update(value=True)), + Modal( + Stack(Text('This is a sample Modal'), justify='center', align='center'), + show=show_modal, + ), + show_code( + modal_var, + """ +Stack( + Button('Click to show modal', onclick=show_modal.update(value=True)), + Modal( + Stack(Text('This is a sample Modal'), justify='center', align='center'), + show=show_modal, + ), +) + """, + 'common', + 'modal', + ), + ) + + +overlay_var = Variable(False) + + +def overlay() -> ComponentInstance: + return Stack( + Label(Switch(value=show_overlay), value='Show Overlay:'), + Overlay(Text('Overlay Text'), show=show_overlay), + show_code( + overlay_var, + """ +Stack( + Label(Switch(value=show_overlay), value='Show Overlay:'), + Overlay(Text('Overlay Text'), show=show_overlay), +) + """, + 'common', + 'overlay', + ), + ) + + +paragraph_var = Variable(False) + + +def paragraph() -> ComponentInstance: + return Stack( + Paragraph( + Text( + 'This is a nice way of combining different Text components, this is especially useful as it allows to switch between different styles such as:' + ), + Text('Bold,', bold=True), + Text('Italic,', italic=True), + Text('different font sizes...', raw_css={'font-size': '0.75rem'}), + ), + Text('different font sizes...', raw_css={'font-size': '0.75rem'}), + show_code( + paragraph_var, + """ +Paragraph( + Text( + 'This is a nice way of combining different Text components, this is especially useful as it allows to switch between different styles such as:' + ), + Text('Bold,', bold=True), + Text( + 'Italic,', + italic=True + ), + Text('different font sizes...', raw_css={'font-size': '0.75rem'}), +) + """, + 'common', + 'paragraph', + ), + ) + + +progress_bar_var = Variable(False) + + +def progress_bar() -> ComponentInstance: + return Stack( + ProgressBar(progress=Variable(30), height='3rem'), + show_code( + progress_bar_var, + """ +ProgressBar(progress=Variable(30), height='3rem') + """, + 'common', + 'progress_bar', + ), + ) + + +radio_group_var = Variable(False) + + +def radio_group() -> ComponentInstance: + return Stack( + Label( + RadioGroup( + items=['first', 'second', 'third'], + value=Variable('first'), + ), + value='Vertical:', + ), + Label( + RadioGroup(items=['first', 'second', 'third'], value=Variable('first'), direction='horizontal'), + value='Horizontal:', + ), + show_code( + radio_group_var, + """ +Stack( + Label( + RadioGroup( + items=['first', 'second', 'third'], + value=Variable('first'), + ), + value='Vertical:', + ), + Label( + RadioGroup(items=['first', 'second', 'third'], value=Variable('first'), direction='horizontal'), + value='Horizontal:', + ), +) + """, + 'common', + 'radio_group', + ), + ) + + +select_var = Variable(False) + + +def select() -> ComponentInstance: + return Stack( + Label( + Select(items=['first', 'second', 'third'], height='2.5rem'), + value='Simple:', + ), + Label( + Select(items=['first', 'second', 'third', 'fourth', 'fifth'], searchable=True, height='2.5rem'), + value='Searchable:', + ), + Label( + Select(items=['first', 'second', 'third', 'fourth', 'fifth'], multiselect=True, height='2.5rem'), + value='Multiselect:', + ), + show_code( + select_var, + """ +Stack( + Label( + Select(items=['first', 'second', 'third'], height='2.5rem'), + value='Simple:', + ), + Label( + Select(items=['first', 'second', 'third', 'fourth', 'fifth'], searchable=True, height='2.5rem'), + value='Searchable:', + ), + Label( + Select(items=['first', 'second', 'third', 'fourth', 'fifth'], multiselect=True, height='2.5rem'), + value='Multiselect:', + ), +) + """, + 'common', + 'select', + ), + ) + + +slider_var = Variable(False) + + +def slider() -> ComponentInstance: + return Stack( + Label( + Slider(domain=[0.0, 1.0], disable_input_alternative=True), + value='Plain:', + ), + Label( + Slider( + domain=[-10, 10], + step=2, + rail_from_start=False, + rail_labels=['My Slider'], + rail_to_end=True, + ticks=[-9, -5, -1, 1, 5, 9], + value=Variable([-3, 6, 8]), + ), + value='A more complex example:', + ), + show_code( + slider_var, + """ +Stack( + Label( + Slider(domain=[0.0, 1.0], disable_input_alternative=True), + value='Plain:', + ), + Label( + Slider( + domain=[-10, 10], + step=2, + rail_from_start=False, + rail_labels=['My Slider'], + rail_to_end=True, + ticks=[-9, -5, -1, 1, 5, 9], + value=Variable([-3, 6, 8]), + ), + value='A more complex example:', + ), +) + """, + 'common', + 'slider', + ), + ) + + +spacer_var = Variable(False) + + +def spacer() -> ComponentInstance: + return Stack( + Text('Empty Spacer:'), + Spacer(), + Text('Line Spacer:'), + Spacer(line=True), + Text('Custom Line Length Spacers:'), + Spacer(line=True, inset='4rem'), + show_code( + spacer_var, + """ +Stack( + Text('Empty Spacer:'), + Spacer(), + Text('Line Spacer:'), + Spacer(line=True), + Text('Custom Line Length Spacers:'), + Spacer(line=True, inset='4rem'), +) + """, + 'common', + 'spacer', + ), + ) + + +stack_var = Variable(False) + + +def stack() -> ComponentInstance: + return Stack( + Stack( + Text('Vertical Stack:'), + Stack( + Card( + Stack(Text('50%'), align='center', justify='center'), + accent=True, + padding='0px', + ), + Card(Stack(Text('30%'), align='center', justify='center'), accent=True, padding='0px', height='30%'), + Card(Stack(Text('20%'), align='center', justify='center'), accent=True, padding='0px', height='20%'), + width='40%', + ), + Text('Horizontal Stack:'), + Stack( + Card(Stack(Text('30%'), align='center', justify='center'), accent=True, width='30%'), + Card(Stack(Text('20%'), align='center', justify='center'), accent=True, width='20%'), + Card( + Stack(Text('50%'), align='center', justify='center'), + accent=True, + ), + height='30%', + direction='horizontal', + ), + height=450, + ), + show_code( + stack_var, + """ +Stack( + Text('Vertical Stack:'), + Stack( + Card( + Stack(Text('50%'), align='center', justify='center'), + accent=True, + padding='0px', + ), + Card( + Stack(Text('30%'), align='center', justify='center'), + accent=True, + padding='0px', + height='30%' + ), + Card( + Stack(Text('20%'), align='center', justify='center'), + accent=True, + padding='0px', + height='20%' + ), + width='40%', + ), + Text('Horizontal Stack:'), + Stack( + Card( + Stack(Text('30%'), align='center', justify='center'), + accent=True, + width='30%', + ), + Card( + Stack(Text('20%'), align='center', justify='center'), + accent=True, + width='20%', + ), + Card( + Stack(Text('50%'), align='center', justify='center'), + accent=True, + ), + height='30%', + direction='horizontal', + ), + raw_css={'min-height': '450px'}, +) + """, + 'common', + 'stack', + ), + raw_css={'min-height': '460px'}, + ) + + +switch_var = Variable(False) + + +def switch() -> ComponentInstance: + return Stack( + Switch(), + show_code( + switch_var, + """ +Switch() + """, + 'common', + 'switch', + ), + ) + + +tabbed_card_var = Variable(False) + + +def tabbed_card() -> ComponentInstance: + return Stack( + TabbedCard(Tab(Text('Some Text'), title='Tab 1'), Tab(Text('Some Text'), title='Tab 2')), + show_code( + tabbed_card_var, + """ +TabbedCard( + Tab(Text('Some Text'), title='Tab 1'), + Tab(Text('Some Text'), title='Tab 2') + """, + 'common', + 'tabbed_card', + ), + ) + + +table_var = Variable(False) + + +def table() -> ComponentInstance: + columns = [ + Table.column(col_id='col1', label='Col 1', filter=Table.TableFilter.TEXT), + Table.column(col_id='col2', label='Col 2', filter=Table.TableFilter.NUMERIC), + Table.column(col_id='col3', label='Col 3', filter=Table.TableFilter.CATEGORICAL, unique_items=['M', 'F']), + Table.column( + col_id='col4', + label='Col 4', + filter=Table.TableFilter.DATETIME, + formatter={'type': Table.TableFormatterType.DATETIME, 'format': 'dd/MM/yyyy'}, + ), + ] + + return Stack( + Table(columns=columns, data=data), + show_code( + table_var, + """ +data = DataVariable( + DataFrame( + [ + { + 'col1': 'a', + 'col2': 1, + 'col3': 'F', + 'col4': '1990-02-12T00:00:00.000Z', + }, + { + 'col1': 'b', + 'col2': 2, + 'col3': 'M', + 'col4': '1991-02-12T00:00:00.000Z', + }, + { + 'col1': 'c', + 'col2': 3, + 'col3': 'M', + 'col4': '1991-02-12T00:00:00.000Z', + }, + { + 'col1': 'd', + 'col2': 4, + 'col3': 'F', + 'col4': '1994-02-07T00:00:00.000Z', + }, + { + 'col1': 'abc', + 'col2': 4, + 'col3': 'M', + 'col4': '1993-12-12T00:00:00.000Z', + }, + ] + ) +) + +columns = [ + Table.column(col_id='col1', label='Col 1', filter=Table.TableFilter.TEXT), + Table.column(col_id='col2', label='Col 2', filter=Table.TableFilter.NUMERIC), + Table.column(col_id='col3', label='Col 3', filter=Table.TableFilter.CATEGORICAL, unique_items=['M', 'F']), + Table.column( + col_id='col4', + label='Col 4', + filter=Table.TableFilter.DATETIME, + formatter={'type': Table.TableFormatterType.DATETIME, 'format': 'dd/MM/yyyy'}, + ), +] + +Table(columns=columns, data=data, max_rows=5) + """, + 'common', + 'table', + ), + height='400px', + ) + + +text_var = Variable(False) + + +def text() -> ComponentInstance: + return Stack( + Text('A component for displaying text'), + show_code( + text_var, + """ +Text('A component for displaying text') + """, + 'common', + 'text', + ), + ) + + +textarea_var = Variable(False) + + +def textarea() -> ComponentInstance: + return Stack( + Textarea(), + show_code( + textarea_var, + """ +Textarea() + """, + 'common', + 'textarea', + ), + ) + + +tooltip_var = Variable(False) + + +def tooltip() -> ComponentInstance: + return Stack( + Stack( + Tooltip( + Text('Hover me', width='100px'), + content='This is a tooltip!', + ), + align='center', + ), + show_code( + tooltip_var, + """ +Stack( + Tooltip( + Text('Hover me', width='100px'), + content="This is a tooltip!", + ), + align='center', +) + """, + 'common', + 'tooltip', + ), + ) diff --git a/packages/demo-app/poetry.toml b/packages/demo-app/poetry.toml new file mode 100644 index 000000000..9ddaf903e --- /dev/null +++ b/packages/demo-app/poetry.toml @@ -0,0 +1,3 @@ +[virtualenvs] +in-project = true +create = false diff --git a/packages/demo-app/pyproject.toml b/packages/demo-app/pyproject.toml new file mode 100644 index 000000000..eba0fb6bf --- /dev/null +++ b/packages/demo-app/pyproject.toml @@ -0,0 +1,23 @@ + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry] +name = "demo_app" +version = "1.25.4" +description = "Demo App" +authors = ["Author "] +source = [] + +[tool.poetry.dependencies] +python = ">=3.10.0, <3.13.0" +dara-core = "1.25.4" +dara-components = "1.25.4" + +[tool.poetry.dev-dependencies] +ruff = ">=0.12.2" +pyright = "^1.1.400" + + + diff --git a/packages/demo-app/static/dara_light.svg b/packages/demo-app/static/dara_light.svg new file mode 100644 index 000000000..1363fcf62 --- /dev/null +++ b/packages/demo-app/static/dara_light.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/poetry.lock b/poetry.lock index dff9feb07..105105c91 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. [[package]] name = "aiorwlock" @@ -755,6 +755,23 @@ files = [ {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, ] +[[package]] +name = "demo-app" +version = "1.25.4" +description = "Demo App" +optional = false +python-versions = ">=3.10.0, <3.13.0" +files = [] +develop = true + +[package.dependencies] +dara-components = "1.25.4" +dara-core = "1.25.4" + +[package.source] +type = "directory" +url = "packages/demo-app" + [[package]] name = "dill" version = "0.3.9" @@ -2864,13 +2881,13 @@ files = [ [[package]] name = "tenacity" -version = "9.1.3" +version = "9.1.4" description = "Retry code until it succeeds" optional = false python-versions = ">=3.10" files = [ - {file = "tenacity-9.1.3-py3-none-any.whl", hash = "sha256:51171cfc6b8a7826551e2f029426b10a6af189c5ac6986adcd7eb36d42f17954"}, - {file = "tenacity-9.1.3.tar.gz", hash = "sha256:a6724c947aa717087e2531f883bde5c9188f603f6669a9b8d54eb998e604c12a"}, + {file = "tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55"}, + {file = "tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a"}, ] [package.extras] @@ -3466,4 +3483,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = ">=3.10.0,<3.13.0" -content-hash = "2e7b2eb9fb93f27182849175557c92dc3dc05ab429041be4692e875654658b5d" +content-hash = "b07a9bd30d0cd4d44f225193853d4dc7064c41f14af9fb7dcceef23d00f82cb9" diff --git a/pyproject.toml b/pyproject.toml index 2816429ca..d3c13527b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,16 +79,36 @@ types-cachetools = ">=6.0.0.20250525,<7.0.0.0" types-croniter = ">=1.0.10,<2.0.0" types-requests = ">=2.28.1,<3.0.0" -[tool.poetry.dev-dependencies.prettier-config] -path = "packages/prettier-config" +[tool.poetry.dev-dependencies.dara-core] +path = "packages/dara-core" +develop = true + +[tool.poetry.dev-dependencies.ui-hierarchy-viewer] +path = "packages/ui-hierarchy-viewer" develop = true [tool.poetry.dev-dependencies.ui-widgets] path = "packages/ui-widgets" develop = true -[tool.poetry.dev-dependencies.ui-utils] -path = "packages/ui-utils" +[tool.poetry.dev-dependencies.prettier-config] +path = "packages/prettier-config" +develop = true + +[tool.poetry.dev-dependencies.styled-components] +path = "packages/styled-components" +develop = true + +[tool.poetry.dev-dependencies.ui-icons] +path = "packages/ui-icons" +develop = true + +[tool.poetry.dev-dependencies.ui-components] +path = "packages/ui-components" +develop = true + +[tool.poetry.dev-dependencies.stylelint-config] +path = "packages/stylelint-config" develop = true [tool.poetry.dev-dependencies.eslint-config] @@ -99,41 +119,25 @@ develop = true path = "packages/create-dara-app" develop = true -[tool.poetry.dev-dependencies.styled-components] -path = "packages/styled-components" -develop = true - -[tool.poetry.dev-dependencies.ui-components] -path = "packages/ui-components" +[tool.poetry.dev-dependencies.demo_app] +path = "packages/demo-app" develop = true [tool.poetry.dev-dependencies.ui-notification] path = "packages/ui-notifications" develop = true -[tool.poetry.dev-dependencies.ui-hierarchy-viewer] -path = "packages/ui-hierarchy-viewer" -develop = true - [tool.poetry.dev-dependencies.dara-components] path = "packages/dara-components" develop = true -[tool.poetry.dev-dependencies.dara-core] -path = "packages/dara-core" +[tool.poetry.dev-dependencies.ui-utils] +path = "packages/ui-utils" develop = true [tool.poetry.dev-dependencies.ui-causal-graph-editor] path = "packages/ui-causal-graph-editor" develop = true -[tool.poetry.dev-dependencies.ui-icons] -path = "packages/ui-icons" -develop = true - -[tool.poetry.dev-dependencies.stylelint-config] -path = "packages/stylelint-config" -develop = true - [build-system] requires = ["poetry"] From 4f0ce29486304b770e74c322f4e329310704a80b Mon Sep 17 00:00:00 2001 From: Krzysztof Bielikowicz Date: Sat, 7 Feb 2026 15:23:23 +0000 Subject: [PATCH 4/5] Fix tests, remove more defaults --- packages/dara-components/changelog.md | 3 +-- .../dara/components/common/button.py | 6 +++++- .../dara/components/common/select.py | 6 +++++- .../dara/components/common/stack.py | 4 +++- .../dara/components/common/table.py | 4 ++++ .../dara-components/dara/components/common/text.py | 4 ++++ .../dara-components/js/common/select/select.tsx | 4 ++-- packages/dara-components/js/common/stack/stack.tsx | 11 +++++++---- packages/dara-components/js/common/table/table.tsx | 14 +++++++++----- .../tests/python/common/test_button.py | 2 -- .../tests/python/common/test_select.py | 3 --- .../tests/python/common/test_stack.py | 5 ----- .../tests/python/common/test_table.py | 10 ---------- .../tests/python/common/test_text.py | 2 -- packages/dara-core/changelog.md | 2 +- packages/dara-core/js/components/for/for.tsx | 2 +- 16 files changed, 42 insertions(+), 40 deletions(-) diff --git a/packages/dara-components/changelog.md b/packages/dara-components/changelog.md index 9ec897c76..78c2135e2 100644 --- a/packages/dara-components/changelog.md +++ b/packages/dara-components/changelog.md @@ -4,8 +4,7 @@ title: Changelog ## NEXT -- Fixed `Card` component to handle missing `title`/`subtitle` props correctly when `None` values are excluded from serialization. -- Fixed `Text` component to not emit unnecessary inline CSS properties when styling props are at their defaults. +- Improved serialization of commonly useed components to exclude component-specific fields at their default values, reducing payload size. ## 1.25.1 diff --git a/packages/dara-components/dara/components/common/button.py b/packages/dara-components/dara/components/common/button.py index 00b742454..6467b8ed8 100644 --- a/packages/dara-components/dara/components/common/button.py +++ b/packages/dara-components/dara/components/common/button.py @@ -16,7 +16,7 @@ """ from enum import Enum -from typing import cast +from typing import ClassVar, cast from dara.components.common.base_component import LayoutComponent from dara.components.common.text import Text @@ -123,6 +123,10 @@ async def navigate_to(ctx: action.Ctx, url: str): :param stop_click_propagation: Whether to stop the click event from propagating to the parent element, defaults to true """ + _exclude_when_default: ClassVar[frozenset[str]] = LayoutComponent._exclude_when_default | frozenset( + {'outline', 'stop_click_propagation'} + ) + disabled: Condition | ClientVariable | bool | None = None loading: Condition | ClientVariable | bool | None = None onclick: Action | None = None diff --git a/packages/dara-components/dara/components/common/select.py b/packages/dara-components/dara/components/common/select.py index 4a292ace6..c89471b98 100644 --- a/packages/dara-components/dara/components/common/select.py +++ b/packages/dara-components/dara/components/common/select.py @@ -15,7 +15,7 @@ limitations under the License. """ -from typing import Any +from typing import Any, ClassVar from pydantic import ValidationInfo, field_validator @@ -126,6 +126,10 @@ class Select(FormComponent): :param size: An optional font size for the select component, in REM units """ + _exclude_when_default: ClassVar[frozenset[str]] = FormComponent._exclude_when_default | frozenset( + {'multiselect', 'searchable', 'max_rows'} + ) + id: str | None = None multiselect: bool = False searchable: bool = False diff --git a/packages/dara-components/dara/components/common/stack.py b/packages/dara-components/dara/components/common/stack.py index 2c47a481a..eb5a440e6 100644 --- a/packages/dara-components/dara/components/common/stack.py +++ b/packages/dara-components/dara/components/common/stack.py @@ -138,7 +138,9 @@ class Stack(LayoutComponent): # Override to keep 'hug' in the serialized payload. Stack defaults hug=False # which differs from StyledComponentInstance's hug=None. If excluded, the JS client # receives undefined and would incorrectly inherit hug from a parent Grid context. - _exclude_when_default: ClassVar[frozenset[str]] = LayoutComponent._exclude_when_default - frozenset({'hug'}) + _exclude_when_default: ClassVar[frozenset[str]] = ( + LayoutComponent._exclude_when_default - frozenset({'hug'}) + ) | frozenset({'collapsed', 'direction', 'scroll'}) collapsed: Variable[bool] | bool = False direction: Direction = Direction.VERTICAL diff --git a/packages/dara-components/dara/components/common/table.py b/packages/dara-components/dara/components/common/table.py index 1d065abdd..6eb8b66b4 100644 --- a/packages/dara-components/dara/components/common/table.py +++ b/packages/dara-components/dara/components/common/table.py @@ -901,6 +901,10 @@ async def on_action(ctx: action.Ctx): model_config = ConfigDict(ser_json_timedelta='float', use_enum_values=True, arbitrary_types_allowed=True) + _exclude_when_default: ClassVar[frozenset[str]] = ContentComponent._exclude_when_default | frozenset( + {'multi_select', 'searchable', 'include_index', 'show_checkboxes', 'suppress_click_events_for_selection'} + ) + columns: Sequence[Column | dict | str] | ClientVariable | None = None data: AnyVariable | DataFrame | list multi_select: bool = False diff --git a/packages/dara-components/dara/components/common/text.py b/packages/dara-components/dara/components/common/text.py index eae8a5076..ce41c504f 100644 --- a/packages/dara-components/dara/components/common/text.py +++ b/packages/dara-components/dara/components/common/text.py @@ -15,6 +15,8 @@ limitations under the License. """ +from typing import ClassVar + from pydantic import field_validator from dara.components.common.base_component import ContentComponent @@ -51,6 +53,8 @@ class Text(ContentComponent): :param formatted: Whether to display the text with existing formatting intact or not, default False """ + _exclude_when_default: ClassVar[frozenset[str]] = ContentComponent._exclude_when_default | frozenset({'formatted'}) + text: str | ClientVariable align: str | None = None # type: ignore # this is actually textAlign not align-items formatted: bool = False diff --git a/packages/dara-components/js/common/select/select.tsx b/packages/dara-components/js/common/select/select.tsx index b23dda799..db055bcc1 100644 --- a/packages/dara-components/js/common/select/select.tsx +++ b/packages/dara-components/js/common/select/select.tsx @@ -93,7 +93,7 @@ interface SelectProps extends FormComponentProps { * * @param props the component props */ -function Select(props: SelectProps): JSX.Element { +function Select({ max_rows = 3, ...props }: SelectProps): JSX.Element { const formCtx = useFormContext(props); const [items] = useVariable(props.items); const [style, css] = useComponentStyles(props); @@ -222,7 +222,7 @@ function Select(props: SelectProps): JSX.Element { $rawCss={css} className={props.className} items={itemArray} - maxRows={props.max_rows} + maxRows={max_rows} onSelect={onSelect} placeholder={props.placeholder} selectedItems={selectedItems} diff --git a/packages/dara-components/js/common/stack/stack.tsx b/packages/dara-components/js/common/stack/stack.tsx index 4aaa4e224..385f717d0 100644 --- a/packages/dara-components/js/common/stack/stack.tsx +++ b/packages/dara-components/js/common/stack/stack.tsx @@ -43,12 +43,15 @@ const StyledStack = injectCss(styled.div` height: ${(props) => (props.direction === 'horizontal' ? '100%' : undefined)}; `); -function Stack(props: StackProps, ref: ForwardedRef): JSX.Element { +function Stack( + { direction = 'vertical', ...props }: StackProps, + ref: ForwardedRef +): JSX.Element { const [collapsed] = useVariable(props.collapsed); const [style, css] = useComponentStyles(props); const stackContent = ( - + {props.children.map((child, idx) => ( ))} @@ -61,7 +64,7 @@ function Stack(props: StackProps, ref: ForwardedRef): JSX.Elemen $rawCss={css} className={props.className} data-type="children-wrapper" - direction={props.direction} + direction={direction} ref={ref} style={{ alignItems: props.align, @@ -76,7 +79,7 @@ function Stack(props: StackProps, ref: ForwardedRef): JSX.Elemen
>({}); @@ -636,12 +640,12 @@ function Table(props: TableProps): JSX.Element { const mappedCols = mapColumns(resolvedColumns); - if ((props.show_checkboxes && (props.onclick_row || props.onselect_row)) || props.selected_indices) { + if ((show_checkboxes && (props.onclick_row || props.onselect_row)) || props.selected_indices) { mappedCols?.unshift(UiTable.ActionColumn([UiTable.Actions.SELECT], 'select_box_col', 'left', true)); } return mappedCols; - }, [resolvedColumns, props.show_checkboxes, props.onclick_row, props.selected_indices, props.onselect_row]); + }, [resolvedColumns, show_checkboxes, props.onclick_row, props.selected_indices, props.onselect_row]); const onSelect = useCallback( async (row: any, isCheckboxSelect: boolean = false): Promise => { diff --git a/packages/dara-components/tests/python/common/test_button.py b/packages/dara-components/tests/python/common/test_button.py index 15fb008f0..00c28bcb0 100644 --- a/packages/dara-components/tests/python/common/test_button.py +++ b/packages/dara-components/tests/python/common/test_button.py @@ -23,10 +23,8 @@ def test_serialization(self, _uid): 'name': 'Button', 'props': { 'children': [Text(text='Click Here').dict(exclude_none=True)], - 'stop_click_propagation': True, 'onclick': action.dict(exclude_none=True), 'styling': ButtonStyle.PRIMARY.value, - 'outline': False, 'position': 'relative', }, 'uid': str(test_uid), diff --git a/packages/dara-components/tests/python/common/test_select.py b/packages/dara-components/tests/python/common/test_select.py index 7f5747e4f..d42289dc1 100644 --- a/packages/dara-components/tests/python/common/test_select.py +++ b/packages/dara-components/tests/python/common/test_select.py @@ -26,9 +26,6 @@ def test_serialization(self, _uid): {'label': 'test2', 'value': 'test2'}, {'label': 'test3', 'value': 'test3'}, ], - 'max_rows': 3, - 'multiselect': False, - 'searchable': False, 'value': value.dict(exclude_none=True), }, 'uid': str(test_uid), diff --git a/packages/dara-components/tests/python/common/test_stack.py b/packages/dara-components/tests/python/common/test_stack.py index ec83423ce..1a692721a 100644 --- a/packages/dara-components/tests/python/common/test_stack.py +++ b/packages/dara-components/tests/python/common/test_stack.py @@ -26,11 +26,8 @@ def test_serialization(self, _uid): 'name': 'Stack', 'props': { 'children': [t1.dict(exclude_none=True), t2.dict(exclude_none=True)], - 'collapsed': False, - 'direction': Direction.VERTICAL.value, 'hug': False, 'position': 'relative', - 'scroll': False, }, 'uid': 'uid', } @@ -43,11 +40,9 @@ def test_serialization(self, _uid): 'props': { 'align': 'end', 'children': [t1.dict(exclude_none=True), t2.dict(exclude_none=True)], - 'collapsed': False, 'direction': Direction.HORIZONTAL.value, 'hug': False, 'position': 'relative', - 'scroll': False, }, 'uid': 'uid', } diff --git a/packages/dara-components/tests/python/common/test_table.py b/packages/dara-components/tests/python/common/test_table.py index 04d30030b..fdc3949a0 100644 --- a/packages/dara-components/tests/python/common/test_table.py +++ b/packages/dara-components/tests/python/common/test_table.py @@ -38,7 +38,6 @@ def test_serialization(self, _uid): 'name': 'Table', 'props': { 'children': [], - 'include_index': True, 'columns': [ { 'col_id': 'name', @@ -53,10 +52,6 @@ def test_serialization(self, _uid): }, ], 'data': data_var.dict(exclude_none=True), - 'multi_select': False, - 'show_checkboxes': True, - 'searchable': False, - 'suppress_click_events_for_selection': False, }, 'uid': 'uid', } @@ -75,7 +70,6 @@ def test_serialization(self, _uid): 'name': 'Table', 'props': { 'children': [], - 'include_index': True, 'columns': [ { 'col_id': 'col1', @@ -90,10 +84,6 @@ def test_serialization(self, _uid): {'col_id': 'col2', 'label': 'col2'}, ], 'data': data_var.dict(exclude_none=True), - 'multi_select': False, - 'show_checkboxes': True, - 'searchable': False, - 'suppress_click_events_for_selection': False, }, 'uid': 'uid', } diff --git a/packages/dara-components/tests/python/common/test_text.py b/packages/dara-components/tests/python/common/test_text.py index cb00169c8..fffffd8b5 100644 --- a/packages/dara-components/tests/python/common/test_text.py +++ b/packages/dara-components/tests/python/common/test_text.py @@ -15,7 +15,6 @@ def test_serialization(self, _uid): 'name': 'Text', 'props': { 'children': [], - 'formatted': False, 'text': 'Some Text', }, 'uid': 'uid', @@ -28,7 +27,6 @@ def test_serialization(self, _uid): 'props': { 'children': [], 'align': 'center', - 'formatted': False, 'width': '10px', 'text': 'Some Text', }, diff --git a/packages/dara-core/changelog.md b/packages/dara-core/changelog.md index 544332861..d27c50e60 100644 --- a/packages/dara-core/changelog.md +++ b/packages/dara-core/changelog.md @@ -4,7 +4,7 @@ title: Changelog ## NEXT -- Improved component serialization to exclude `None` values and common styling fields at their default values, significantly reducing payload size. +- Improved component serialization to exclude default values for all fields whose default is `None`, reducing payload size. ## 1.25.4 diff --git a/packages/dara-core/js/components/for/for.tsx b/packages/dara-core/js/components/for/for.tsx index 505360120..504545068 100644 --- a/packages/dara-core/js/components/for/for.tsx +++ b/packages/dara-core/js/components/for/for.tsx @@ -113,7 +113,7 @@ function ForImpl(props: ForProps & { suspend: number | boolean }): React.ReactNo return ; } - if (props.virtualization === null) { + if (props.virtualization == null) { // reapply the parent suspend setting return ( From 86d70e9f37c9e4b3c60ebdc3b61429e497f3be9a Mon Sep 17 00:00:00 2001 From: Krzysztof Bielikowicz Date: Sat, 7 Feb 2026 15:36:31 +0000 Subject: [PATCH 5/5] Format --- packages/dara-components/js/common/stack/stack.tsx | 5 +---- packages/dara-components/js/common/table/table.tsx | 6 +----- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/packages/dara-components/js/common/stack/stack.tsx b/packages/dara-components/js/common/stack/stack.tsx index 385f717d0..2f0f2fa5c 100644 --- a/packages/dara-components/js/common/stack/stack.tsx +++ b/packages/dara-components/js/common/stack/stack.tsx @@ -43,10 +43,7 @@ const StyledStack = injectCss(styled.div` height: ${(props) => (props.direction === 'horizontal' ? '100%' : undefined)}; `); -function Stack( - { direction = 'vertical', ...props }: StackProps, - ref: ForwardedRef -): JSX.Element { +function Stack({ direction = 'vertical', ...props }: StackProps, ref: ForwardedRef): JSX.Element { const [collapsed] = useVariable(props.collapsed); const [style, css] = useComponentStyles(props); diff --git a/packages/dara-components/js/common/table/table.tsx b/packages/dara-components/js/common/table/table.tsx index df2cd20bd..dfb81abc1 100644 --- a/packages/dara-components/js/common/table/table.tsx +++ b/packages/dara-components/js/common/table/table.tsx @@ -508,11 +508,7 @@ function resolveColumns( const TableWrapper = injectCss('div'); -function Table({ - include_index = true, - show_checkboxes = true, - ...props -}: TableProps): JSX.Element { +function Table({ include_index = true, show_checkboxes = true, ...props }: TableProps): JSX.Element { const getData = useTabularVariable(props.data); const [selectedRowIndices, setSelectedRowIndices] = useVariable(props.selected_indices);