diff --git a/examples/apps/dashboard/dashboard.py b/examples/apps/dashboard/dashboard.py new file mode 100644 index 00000000..bdd6571d --- /dev/null +++ b/examples/apps/dashboard/dashboard.py @@ -0,0 +1,223 @@ +# Inspiration: https://demos.creative-tim.com/material-dashboard/pages/dashboard +# docs: https://holoviz-dev.github.io/panel-material-ui/ +# panel serve examples/apps/dashboard/*.py --dev +import panel_material_ui as pmu +import panel as pn +import param +import pandas as pd + +from panel_material_ui import ChangeIndicator +from panel_material_ui import Icon + +from shared.data import get_project_data +from shared.plots import ( + get_website_views_config, + get_daily_sales_config, + get_completed_tasks_config, +) +from shared.components import create_menu, Timeline +from shared.page import create_page + +my_theme = { + "palette": { + "primary": { + "main": "#FF5733", + # light, dark, and contrastText can be automatically computed + }, + "secondary": { + "main": "#E0C2FF", + "light": "#F5EBFF", # optional + "dark": "#BA99D5", # optional + "contrastText": "#47008F", # optional + } + } +} + + +pn.extension("echarts") + +page_title = f"""\ +## Dashboard + +Check the sales, value and bounce rate by country.\ +""" + +indicators = pn.Row( + ChangeIndicator( + title="Today's Money", icon="weekend", value="$53,000", change_percent=55, since="since yesterday", sizing_mode="stretch_width" + ), + ChangeIndicator(title="Today's Users", icon="person", value="2300", change_percent=3, since="than last month", sizing_mode="stretch_width"), + ChangeIndicator(title="Ads Views", icon="leaderboard", value="3,462", change_percent=-2, since="since yesterday", sizing_mode="stretch_width"), + ChangeIndicator(title="Sales", icon="weekend", value="$103,430", change_percent=5, since="since yesterday", sizing_mode="stretch_width"), +) + + +def last_update(message): + schedule = Icon(value="schedule", font_size="small", sizing_mode="fixed", width=20, height=25, margin=(10,5, 10, 5)) + return pn.Row(schedule, pn.pane.HTML(message, sizing_mode="fixed", margin=(11,0)), sizing_mode="fixed") + +def generate_company_html(row, image_width='40px'): + company_name=row["Company"] + company_image_url=row["CompanyImage"] + html = f''' +
+ {company_name} +

{company_name}

+
+ ''' + return html + +def generate_progress_bar(row): + completion_percent = int(round(float(row["Completion"]),0)) + color = 'green' if completion_percent >=99.999 else 'blue' + html = f''' +
+
{completion_percent}%
+
+
+
+
+ ''' + return html + +def generate_member_images(row, image_size='30px', overlap_offset='-10px'): + members = row["Members"] + images_html = '' + for idx, member in enumerate(members): + images_html += f''' + + ''' + wrapper_html = f''' +
+ {images_html} +
+ ''' + return wrapper_html + +def to_styled_projects_table(data: pd.DataFrame): + data["Company"] = data.apply(generate_company_html, axis=1) + data["Completion"] = data.apply(generate_progress_bar, axis=1) + data["Members"] = data.apply(generate_member_images, axis=1) + data = data.drop(columns=["CompanyImage"]) + styled_df = data.style + styled_df = ( + styled_df.hide(axis="index") + .set_table_styles( + [ + { + "selector": "th", + "props": [ + ("background-color", "white"), + ("font-weight", "bold"), + ("border-bottom", "1px solid black"), + ("text-align", "left"), + ], + }, + { + "selector": "td", + "props": [ + ("padding", "10px"), + ("background", "white"), + ("border-top", "1px solid black"), + ], + }, + ] + ) + .set_properties( + **{"text-align": "left"}, + ) + ) + + return styled_df + + +with pn.config.set(sizing_mode="stretch_width"): + web_site_views = pmu.Paper( + "#### Website Views\n\nLast Campaign Performance", + pn.pane.ECharts( + get_website_views_config(), + height=250, + margin=(0, 5), + ), + pn.Spacer(sizing_mode="stretch_height"), + pmu.Divider(margin=(10, 10, -5, 10)), + last_update("campaign sent 2 days ago"), + height=400, margin=10 + ) + + daily_sales = pmu.Paper( + "#### Daily Sales\n\n**+15%** increase in todays sales", + pn.pane.ECharts( + get_daily_sales_config(), + height=250, + margin=(0, 5), + ), + pn.Spacer(sizing_mode="stretch_height"), + pmu.Divider(margin=(10,10,-5,10)), + last_update("updated 4 min ago"), + margin=10, + height=400, + ) + completed_tasks = pmu.Paper( + "#### Completed Tasks\n\nLast Campaign Performance", + pn.pane.ECharts( + get_completed_tasks_config(), + height=250, + margin=(0, 5), + ), + pn.Spacer(sizing_mode="stretch_height"), + pmu.Divider(margin=(10,10,-5,10)), + last_update("just updated"), + margin=10, + height=400, + ) + + plots = pn.Row(web_site_views, daily_sales, completed_tasks, sizing_mode="stretch_both") + project_table = pmu.Paper( + "#### Projects\n\n**30 done** this month", + pn.pane.DataFrame(to_styled_projects_table(get_project_data()), sizing_mode="stretch_width"), + margin=10, + height=550, + ) + timeline_config = [ + {"content_title": "$2400, Design changes", "content": "22 DEC 7:20 PM", "color": "success", "icon": "notifications", "disable_dot": True}, + {"content_title": "New order #1832412", "content": "21 DEC 11 PM", "color": "error", "icon": "code", "disable_dot": True}, + {"content_title": "Server payments for April", "content": "21 DEC 9:34 PM", "color": "primary", "icon": "shopping_cart", "disable_dot": True}, + {"content_title": "New card added for order #4395133", "content": "20 DEC 2:20 AM", "color": "warning", "icon": "credit_card", "disable_dot": True}, + {"content_title": "Unlock packages for development", "content": "18 DEC 4:54 AM", "color": "error", "icon": "key", "disable_dot": True}, + {"content_title": "New order #9583120", "content": "17 DEC", "color": "dark", "icon": "payments", "disable_dot": True}, + ] + sx = { + "& .MuiTimelineItem-root:before": { + "flex": 0, + "padding-left": 10, + }, + } + timeline_component = Timeline(object=timeline_config, sizing_mode="stretch_width", sx=sx) + timeline_component = pmu.Alert("The `Timeline` component does not work yet due to [#190](https://github.com/panel-extensions/panel-material-ui/issues/190).", alert_type="error", margin=10) + timeline = pmu.Paper( + "#### Orders overview\n\n**24%** this month", + timeline_component, + margin=10, + height=550, + ) + table_timeline_row = pn.Row(project_table, timeline) + +create_page( + name="Dashboard", + main=[page_title, indicators, plots, table_timeline_row]).servable( + title="Dashboard" +) diff --git a/examples/apps/dashboard/notifications.py b/examples/apps/dashboard/notifications.py new file mode 100644 index 00000000..aad8a096 --- /dev/null +++ b/examples/apps/dashboard/notifications.py @@ -0,0 +1,29 @@ +import panel as pn +from shared.page import create_page +from panel_material_ui import Button, Alert +pn.extension(notifications=True) + +with pn.config.set(sizing_mode="stretch_width"): + alert_card = pn.Column( + """## Alerts + +Notifications on this page use the `Alert` from Material UI. Read more details [here](https://mui.com/material-ui/react-alert/). +""", + *[Alert(title=f"A simple {severity} alert with an example link. Give it a click if you like.", severity=severity, variant="filled", closeable=True, margin=10) for severity in Alert.param.severity.objects], + max_width=1200, + ) + + success_notification = pn.widgets.Button(name="Success") + notifications_row = pn.Column( + """## Notifications + +Notifications on this page use the `SnackbarProvider` from notistack. Read more details [here](https://notistack.com/getting-started).""", + pn.Row( + Button(name="Error", color="error", sizing_mode="fixed", width=100, on_click=lambda e: pn.state.notifications.error('This is an error notification.', duration=1000)), + Button(name="Info", color="info", sizing_mode="fixed", width=100, on_click=lambda e: pn.state.notifications.info('This is an info notification.', duration=1000)), + Button(name="Success", color="success", sizing_mode="fixed", width=100, on_click=lambda e: pn.state.notifications.success('This is a success notification.', duration=1000)), + Button(name="Warning", color="warning", sizing_mode="fixed", width=100, on_click=lambda e: pn.state.notifications.warning('This is a warning notification.', duration=1000)), + ) + ) + +create_page("Notifications", main=[alert_card, notifications_row]).servable(title="Notifications") diff --git a/examples/apps/dashboard/shared/Timeline.jsx b/examples/apps/dashboard/shared/Timeline.jsx new file mode 100644 index 00000000..96ffd477 --- /dev/null +++ b/examples/apps/dashboard/shared/Timeline.jsx @@ -0,0 +1,59 @@ +import { + Timeline as MUITimeline, + TimelineItem, + TimelineSeparator, + TimelineConnector, + TimelineContent, + TimelineOppositeContent, + TimelineDot, +} from "@mui/lab"; +import {Icon} from "@mui/material" +import Typography from "@mui/material/Typography"; + +export function render({model}) { + const [items] = model.useState("object") + const [position] = model.useState("position") + const [sx] = model.useState("sx") + + return ( + + {items.map((item, idx) => ( + + {(item.opposite !== undefined || item.opposite_title !== undefined) && ( + + + {item.opposite_title} + + + {item.opposite} + + + )} + + {(item.disable_dot && item.icon) ? ( + {item.icon} + ) : ( + + {item.icon !== undefined && ( + {item.icon} + )} + + )} + {idx < items.length - 1 && } + + {(item.content !== undefined || item.content_title !== undefined) && ( + + + {item.content_title} + + {item.content} + + )} + + ))} + + ) +} diff --git a/examples/apps/dashboard/shared/__init__.py b/examples/apps/dashboard/shared/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/apps/dashboard/shared/components.py b/examples/apps/dashboard/shared/components.py new file mode 100644 index 00000000..62169fc1 --- /dev/null +++ b/examples/apps/dashboard/shared/components.py @@ -0,0 +1,78 @@ +import panel as pn +import panel_material_ui as pmu +from .config import PAGES +import param +import copy + +def create_menu(selected: str, pages: list[tuple] = PAGES, button_color="primary"): + active = -1 + for index, page in enumerate(pages): + if selected==page["label"]: + active=index + + def get_pages(button_color): + pages2 = copy.deepcopy(pages) + for item in pages2: + item["color"]=button_color + return pages2 + return pmu.List(items=pn.bind(get_pages, button_color), active=active, margin=(0,10), sizing_mode="stretch_width") + +def create_card_with_jumbo_header(title: str, content: str): + return pmu.Paper( + pn.pane.Markdown( + title, + sizing_mode="stretch_width", + styles={ + "background": "black", + "color": "white", + "border-radius": "5px", + "position": "relative", + "padding-left": "10px", + }, + margin=10, + ), + content, + margin=10, + ) + +class Timeline(pmu.MaterialUIComponent): + """Material‑UI **Timeline** component for Panel. + + References: + + - https://mui.com/material-ui/react-timeline/ + + Example: + + >>> config = [ + ... {"content_title": "Eat", "content": "Because you need strength", "opposite": "08:30", "color": "grey", "variant": "filled", "icon": "fastfood"}, + ... {"content_title": "Code", "content": "Because it's awesome!", "opposite": "09:00", "color": "primary", "variant": "filled", "icon": "laptop_mac"}, + ... {"content_title": "Sleep", "content": "Because you need rest", "opposite": "09:30", "color": "secondary", "variant": "outlined", "icon": "hotel"}, + ... {"content_title": "Repeat", "content": "Because this is the life you love!", "opposite": "11:00", "color": "success", "variant": "filled", "icon": "repeat"}, + ... ] + >>> Timeline(object=config, width=600) + """ # noqa: E501 + object = param.List(default=[], doc=""" + A list of dictionaries, each mapping directly onto a `TimelineItem` row. + Supported keys: + + | key | type | default | description | + |-------------------|------|---------|-------------------------------------------------------------------------------------------------------------------| + | **content_title** | str | *None* | Header of `TimelineContent`. | + | **content** | str | *None* | Body of `TimelineContent`. | + | **opposite_title**| str | *None* | Header of `TimelineOppositeContent`. | + | **opposite** | str | *None* | Body of `TimelineOppositeContent`. | + | **color** | str | primary | Color prop of `TimelineDot`. | + | **variant** | str | filled | Variant prop of `TimelineDot` (filled/ outlined). | + | **icon** | str | *None* | Lowercase name of `Icon`. | + | **disable_dot** | bool | *None* | If `True`, the `Icon` is shown standalone and not inside the `TimelineDot`. | + """) + + position = param.Selector(default="right", objects=[ + 'alternate-reverse', + 'alternate', + 'left', + 'right', + ], doc="""The position of the content/ opposite content.""") + + _esm_base = "Timeline.jsx" diff --git a/examples/apps/dashboard/shared/config.py b/examples/apps/dashboard/shared/config.py new file mode 100644 index 00000000..a0de05dc --- /dev/null +++ b/examples/apps/dashboard/shared/config.py @@ -0,0 +1,55 @@ +import panel as pn +import param +from panel_material_ui.base import COLORS as _BUTTON_COLORS, COLOR_ALIASES + +BODY_BACKGROUND = "#f5f5f5" +ICON_CSS = """ +.material-symbols-outlined { + font-size: 12px; +} +""" +PAPER_STYLES = { + "background": "white", + "border-radius": "5px", + "border": "1px solid #e5e5e5", + "box-shadow": "0 1px 2px 0 rgba(0,0,0,.05)", +} +PAGES = [ + {"label": "Dashboard", "href": "dashboard", "icon": "dashboard"}, + {"label": "Tables", "href": "tables", "icon": "table_view"}, + {"label": "Notifications", "href": "notifications", "icon": "notifications"}, +] + +pn.config.css_files=[ + # "https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0&icon_names=schedule,weekend,person,leaderboard" +] + +_SIDEBAR_BACKGROUND = {"Dark": "#42424a", "Transparent": "inherit", "White": "white"} +_SIDEBAR_COLOR = {"Dark": "white", "Transparent": "inherit", "White": "black"} +_BUTTON_COLORS = [color for color in _BUTTON_COLORS if color not in COLOR_ALIASES] + +class PageSettings(param.Parameterized): + dark_theme = param.Boolean(default=False) + + sidebar_button_color = param.Selector(default="dark", objects=_BUTTON_COLORS) + sidebar_background_color = param.Selector(default="White", objects=["Dark", "Transparent", "White"]) + navbarimportfixed = param.Boolean(default=False) + + def __init__(self, **params): + super().__init__(**params) + + pn.state.location.sync(self, parameters=["dark_theme", "sidebar_button_color", "sidebar_background_color"]) + + @param.depends("dark_theme", watch=True) + def _handle_dark_theme_change(self): + pn.config.theme=("dark" if self.dark_theme else "default") + + @param.depends("sidebar_background_color") + def sidebar_styles(self): + return {"background": _SIDEBAR_BACKGROUND[self.sidebar_background_color], "color": "_SIDEBAR_COLOR", "border-radius": "5px", "margin": "10px"} + + @param.depends("dark_theme") + def body_styles(self): + if not self.dark_theme: + return {"background": BODY_BACKGROUND, "width": "100vw"} # Hack + return {"background": "transparent", "width": "100vw"} diff --git a/examples/apps/dashboard/shared/data.py b/examples/apps/dashboard/shared/data.py new file mode 100644 index 00000000..ed194157 --- /dev/null +++ b/examples/apps/dashboard/shared/data.py @@ -0,0 +1,161 @@ +import pandas as pd + +_AUTHORS_DATA = [ + { + "image": "https://randomuser.me/api/portraits/men/1.jpg", + "name": "John Michael", + "email": "john.michael@example.com", + "title": "Manager", + "team": "Organization", + "status": "Online", + "employed": "23/04/18", + }, + { + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Alexa Liras", + "email": "alexa.liras@example.com", + "title": "Programmer", + "team": "Developer", + "status": "Offline", + "employed": "11/01/19", + }, + { + "image": "https://randomuser.me/api/portraits/men/3.jpg", + "name": "Laurent Perrier", + "email": "laurent.perrier@example.com", + "title": "Executive", + "team": "Projects", + "status": "Online", + "employed": "19/09/17", + }, + { + "image": "https://randomuser.me/api/portraits/women/4.jpg", + "name": "Michael Levi", + "email": "michael.levi@example.com", + "title": "Designer", + "team": "Creative", + "status": "Online", + "employed": "24/12/08", + }, + ] + +_PROJECTS_DATA = [ + { + "Company": "Material XD Version", + "CompanyImage": "https://demos.creative-tim.com/material-dashboard/assets/img/small-logos/logo-xd.svg", + "Members": [ + {"Name": "Ryan Tompson", "Image": "https://demos.creative-tim.com/material-dashboard/assets/img/team-1.jpg"}, + {"Name": "Romina Hadid", "Image": "https://demos.creative-tim.com/material-dashboard/assets/img/team-2.jpg"}, + {"Name": "Alexander Smith", "Image": "https://demos.creative-tim.com/material-dashboard/assets/img/team-3.jpg"}, + {"Name": "Jessica Doe", "Image": "https://demos.creative-tim.com/material-dashboard/assets/img/team-4.jpg"}, + ], + "Budget": "$14,000", + "Completion": 60, + }, + { + "Company": "Add Progress Track", + "CompanyImage": "https://demos.creative-tim.com/material-dashboard/assets/img/small-logos/logo-atlassian.svg", + "Members": [ + {"Name": "Romina Hadid", "Image": "https://demos.creative-tim.com/material-dashboard/assets/img/team-2.jpg"}, + {"Name": "Jessica Doe", "Image": "https://demos.creative-tim.com/material-dashboard/assets/img/team-4.jpg"}, + ], + "Budget": "$3,000", + "Completion": 10, + }, + { + "Company": "Fix Platform Errors", + "CompanyImage": "https://demos.creative-tim.com/material-dashboard/assets/img/small-logos/logo-slack.svg", + "Members": [ + {"Name": "Jessica Doe", "Image": "https://demos.creative-tim.com/material-dashboard/assets/img/team-4.jpg"}, + {"Name": "Romina Hadid", "Image": "https://demos.creative-tim.com/material-dashboard/assets/img/team-2.jpg"}, + ], + "Budget": "Not set", + "Completion": 100, + }, + { + "Company": "Launch our Mobile App", + "CompanyImage": "https://demos.creative-tim.com/material-dashboard/assets/img/small-logos/logo-spotify.svg", + "Members": [ + {"Name": "Ryan Tompson", "Image": "https://demos.creative-tim.com/material-dashboard/assets/img/team-2.jpg"}, + {"Name": "Romina Hadid", "Image": "https://demos.creative-tim.com/material-dashboard/assets/img/team-3.jpg"}, + {"Name": "Alexander Smith", "Image": "https://demos.creative-tim.com/material-dashboard/assets/img/team-4.jpg"}, + {"Name": "Jessica Doe", "Image": "https://demos.creative-tim.com/material-dashboard/assets/img/team-1.jpg"}, + ], + "Budget": "$20,500", + "Completion": 100, + }, + { + "Company": "Add the New Pricing Page", + "CompanyImage": "https://demos.creative-tim.com/material-dashboard/assets/img/small-logos/logo-jira.svg", + "Members": [ + {"Name": "Ryan Tompson", "Image": "https://demos.creative-tim.com/material-dashboard/assets/img/team-4.jpg"}, + ], + "Budget": "$500", + "Completion": 25, + }, + { + "Company": "Redesign New Online Shop", + "CompanyImage": "https://demos.creative-tim.com/material-dashboard/assets/img/small-logos/logo-invision.svg", + "Members": [ + {"Name": "Ryan Tompson", "Image": "https://demos.creative-tim.com/material-dashboard/assets/img/team-1.jpg"}, + {"Name": "Jessica Doe", "Image": "https://demos.creative-tim.com/material-dashboard/assets/img/team-4.jpg"}, + ], + "Budget": "$2,000", + "Completion": 40, + } +] + +_PROJECTS_DATA_2 = [ + { + "Company": "Asana", + "CompanyImage": "https://demos.creative-tim.com/material-dashboard/assets/img/small-logos/logo-asana.svg", + "Budget": "$2,500", + "Status": "working", + "Completion": 60, + }, + { + "Company": "Add Progress Track", + "CompanyImage": "https://demos.creative-tim.com/material-dashboard/assets/img/small-logos/logo-atlassian.svg", + "Budget": "$5,000", + "Status": "done", + "Completion": 10, + }, + { + "Company": "Fix Platform Errors", + "CompanyImage": "https://demos.creative-tim.com/material-dashboard/assets/img/small-logos/logo-slack.svg", + "Budget": "$3,400", + "Status": "canceled", + "Completion": 100, + }, + { + "Company": "Launch our Mobile App", + "CompanyImage": "https://demos.creative-tim.com/material-dashboard/assets/img/small-logos/logo-spotify.svg", + "Budget": "$14,000", + "Status": "working", + "Completion": 100, + }, + { + "Company": "Add the New Pricing Page", + "CompanyImage": "https://demos.creative-tim.com/material-dashboard/assets/img/small-logos/logo-jira.svg", + "Budget": "$1,000", + "Status": "canceled", + "Completion": 25, + }, + { + "Company": "Redesign New Online Shop", + "CompanyImage": "https://demos.creative-tim.com/material-dashboard/assets/img/small-logos/logo-invision.svg", + "Budget": "$2,300", + "Status": "done", + "Completion": 40, + } +] + + +def get_authors_data(): + return pd.DataFrame(_AUTHORS_DATA) + +def get_project_data(): + return pd.DataFrame(_PROJECTS_DATA) + +def get_project_data_2(): + return pd.DataFrame(_PROJECTS_DATA_2) diff --git a/examples/apps/dashboard/shared/page.py b/examples/apps/dashboard/shared/page.py new file mode 100644 index 00000000..067a8edb --- /dev/null +++ b/examples/apps/dashboard/shared/page.py @@ -0,0 +1,183 @@ +import panel as pn +import panel_material_ui as pmu +from shared.components import create_menu +from shared.config import PageSettings + + +def create_sidebar(name: str, button_color, settings: PageSettings): + documentation_link = pmu.widgets.Button( + name="Documentation", + variant="outlined", + href="https://holoviz-dev.github.io/panel-material-ui/", + sizing_mode="stretch_width", + ) + reference_link = pmu.widgets.Button( + name="Reference", + variant="contained", + href="https://demos.creative-tim.com/material-dashboard/pages/dashboard", + color="dark", + sizing_mode="stretch_width", + ) + + return pn.Column( + "#### Panel Material UI", + pmu.layout.Divider(sizing_mode="stretch_width", margin=(10, 5, 10, 5)), + create_menu(name, button_color=button_color), + pn.Spacer(sizing_mode="stretch_height"), + documentation_link, + reference_link, + styles=settings.sidebar_styles, + width=300, + sizing_mode="stretch_height", + ) + + +def create_github_star_count(): + return pn.panel( + """\ + + GitHub stars + \ + """, + align="center", + ) + +def create_context(settings: PageSettings): + def click(event): + button = event.obj + settings.sidebar_button_color=button.name + + colors = list(settings.param.sidebar_button_color.objects)[1:7] + buttons = [pmu.IconButton(color=color, name=color, icon='circle', description='Select color', margin=0, on_click=click) for color in colors] + + theme_toggle = pmu.ThemeToggle(value=settings.dark_theme, variant="switch") + theme_toggle.rx.watch(lambda v: settings.param.update(dark_theme=v)) + + + return pmu.Drawer( + pn.Column( + "## Material UI Configurator", + pn.Row(*buttons, align="center"), + """### Sidenav Type + +Choose between different sidenav types.""", + pmu.widgets.RadioButtonGroup.from_param(settings.param.sidebar_background_color, align="center"), + pmu.layout.Divider(sizing_mode="stretch_width", margin=5), + theme_toggle, + pmu.layout.Divider(sizing_mode="stretch_width", margin=5), + pmu.widgets.Button(name="Free Download", href="https://github.com/panel-extensions/panel-material-ui", color="primary", variant="contained", sizing_mode="stretch_width"), + pmu.widgets.Button(name="Documentation", href="https://holoviz-dev.github.io/panel-material-ui/", variant="outlined", sizing_mode="stretch_width"), + create_github_star_count(), + pn.pane.Markdown("## Thank you for sharing!", align="center"), + pn.Row( + pmu.widgets.Button(name="Tweet", href="https://twitter.com/intent/tweet?text=Check%20Panel%20Material%20UI%20Dashboard%20made%20by%20%40HoloViz%20%23webdesign%20%23dashboard%20%23dataviz&url=https%3A%2F%2Fholoviz-dev.github.io%2Fpanel-material-ui%2F", color="dark", variant="contained"), + pmu.widgets.Button(name="Share", href="https://www.facebook.com/sharer/sharer.php?u=https://holoviz-dev.github.io/panel-material-ui", color="dark", variant="contained"), + align="center" + ), + sizing_mode="stretch_width", + ), + anchor="right", size=400 + ) + + + +def create_header(name: str, settings_callback): + items = [ + {'label': 'Home', "href": "./"}, + {'label': f'{name}'}, + ] + bread_crumbs = pmu.menus.Breadcrumbs( + name="Breadcrumbs test", items=items, align="center" + ) + search = pmu.widgets.TextInput( + name="Type here...", placeholder="Search", size="small" + ) + goto_online_pmu = pmu.widgets.Button( + name="Panel Material UI", + variant="outlined", + color="info", + href="https://panel.holoviz.org", + align="center", + ) + github_star_count = create_github_star_count() + settings_button = pmu.widgets.ButtonIcon( + name="Context", icon="settings", align="center", on_click=settings_callback + ) + + return pn.Row( + bread_crumbs, + pn.Spacer(sizing_mode="stretch_width"), + search, + goto_online_pmu, + github_star_count, + settings_button, + sizing_mode="stretch_width", + ) + + +def create_footer(): + return pn.Row( + "© 2025, made with by **panel-material-ui** for beautiful data apps.", + pn.Spacer(sizing_mode="stretch_width"), + pmu.widgets.Button( + name="Panel Material UI", + variant="text", + href="https://panel-extensions.github.io/panel-material-ui/", + ), + pmu.widgets.Button( + name="About Us", + variant="text", + href="https://panel.holoviz.org", + ), + pmu.widgets.Button( + name="Blog", + variant="text", + href="https://blog.holoviz.org/", + ), + pmu.widgets.Button( + name="License", + variant="text", + href="https://panel-extensions.github.io/panel-material-ui/LICENSE.md", + ) + ) + + +def create_fab(settings_callback): + sx ={ + "position": 'fixed', + "bottom": "64px", + "right": 24, + } + fab_button = pmu.Fab(color='default', label='Click me', icon="settings", size="large", description="Toggle settings drawer", margin=0, on_click=settings_callback, sx=sx) + return fab_button + + +def create_page(name: str, main: list): + settings = PageSettings() + + sidebar = create_sidebar(name=name, button_color=settings.param.sidebar_button_color, settings=settings) + + context = create_context(settings=settings) + + def toggle_drawer(event): + context.open = not context.open + + header = create_header(name=name, settings_callback=toggle_drawer) + + footer = create_footer() + fab_row = create_fab(settings_callback=toggle_drawer) + main = pn.Column( + header, + *main, + pn.layout.VSpacer(), + fab_row, + footer, + sizing_mode="stretch_width", + margin=10, + ) + + return pn.Column( + pn.Row(sidebar, main, context), + sizing_mode="stretch_both", + styles=settings.body_styles, + ) diff --git a/examples/apps/dashboard/shared/plots.py b/examples/apps/dashboard/shared/plots.py new file mode 100644 index 00000000..035517da --- /dev/null +++ b/examples/apps/dashboard/shared/plots.py @@ -0,0 +1,70 @@ +_WEBSITE_VIEWS_DATA = { + "xAxis": {"data": ["M", "T", "W", "T", "F", "S", "S"]}, + "tooltip": {"trigger": "axis", "axisPointer": {"type": "shadow"}}, + "yAxis": {}, + "series": [ + { + "name": "Views", + "type": "bar", + "data": [50, 45, 30, 35, 50, 60, 75], + "itemStyle": {"color": "green"}, + } + ], + "grid": { + "top": 10, + "bottom": 20, + }, +} + +_DAILY_SALES = { + "xAxis": { + "type": "category", + "data": ["J", "F", "M", "A", "M", "J", "J", "A", "S", "O", "N", "D"], + }, + "yAxis": {"type": "value"}, + "series": [ + { + "name": "Monthly Data", + "type": "line", + "data": [120, 132, 101, 134, 90, 230, 210, 180, 150, 200, 170, 250], + "itemStyle": {"color": "green"}, + } + ], + "grid": { + "top": 10, + "bottom": 20, + }, + "tooltip": {"trigger": "axis", "axisPointer": {"type": "shadow"}}, +} +_COMPLETED_TASKS = { + "xAxis": { + "type": "category", + "data": ["Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], + }, + "yAxis": {"type": "value"}, + "series": [ + { + "name": "Monthly Data", + "type": "line", + "data": [50, 50, 300, 210, 500, 230, 400, 230, 525], + "itemStyle": {"color": "green"}, + } + ], + "grid": { + "top": 10, + "bottom": 20, + }, + "tooltip": {"trigger": "axis", "axisPointer": {"type": "shadow"}}, +} + + +def get_website_views_config(): + return _WEBSITE_VIEWS_DATA + + +def get_daily_sales_config(): + return _DAILY_SALES + + +def get_completed_tasks_config(): + return _COMPLETED_TASKS diff --git a/examples/apps/dashboard/shared/tables.py b/examples/apps/dashboard/shared/tables.py new file mode 100644 index 00000000..f35ea8fa --- /dev/null +++ b/examples/apps/dashboard/shared/tables.py @@ -0,0 +1,71 @@ +import pandas as pd + +def generate_company_html(row: pd.Series, image_width: str='40px'): + company_name=row["Company"] + company_image_url=row["CompanyImage"] + html = f''' +
+ {company_name} +

{company_name}

+
+ ''' + return html + +def generate_progress_bar(row: pd.Series): + completion_percent = int(round(float(row["Completion"]),0)) + color = 'green' if completion_percent >=99.999 else 'blue' + html = f''' +
+
{completion_percent}%
+
+
+
+
+ ''' + return html + +def generate_member_images(row: pd.Series, image_size: str='30px', overlap_offset: str='-10px'): + members = row["Members"] + images_html = '' + for idx, member in enumerate(members): + images_html += f''' + + ''' + wrapper_html = f''' +
+ {images_html} +
+ ''' + return wrapper_html + +def render_author(row: pd.Series): + return ( + f'
' + f'img' + f'
{row["name"]}
{row["email"]}
' + f"
" + ) + +def render_function(row: pd.Series): + return f'
{row["title"]}
{row["team"]}
' + +def render_status(row: pd.Series): + color = "#43a047" if row["status"] == "Online" else "#747b8a" + + return f'{row["status"]}' + +def render_edit_button(): + return '' diff --git a/examples/apps/dashboard/tables.py b/examples/apps/dashboard/tables.py new file mode 100644 index 00000000..79a90f28 --- /dev/null +++ b/examples/apps/dashboard/tables.py @@ -0,0 +1,130 @@ +import panel as pn +import panel_material_ui as pmu +import pandas as pd + +from shared.data import get_authors_data, get_project_data_2 +from shared.page import create_page +from shared.components import create_card_with_jumbo_header +from shared import tables as table_func + +pn.extension() + + +@pn.cache +def to_styled_authors_table(data): + data["Author"] = data.apply(table_func.render_author, axis=1) + data["Function"] = data.apply(table_func.render_function, axis=1) + data["Status"] = data.apply(table_func.render_status, axis=1) + data["Edit"] = table_func.render_edit_button() + data = data.rename(columns={"employed": "Employed"}) + data = data[ + [ + "Author", + "Function", + "Status", + "Employed", + "Edit", + ] + ] + + styled_df = data.style + styled_df = ( + styled_df.hide(axis="index") + .set_table_styles( + [ + { + "selector": "th", + "props": [ + ("background-color", "white"), + ("font-weight", "bold"), + ("border-bottom", "1px solid black"), + ("text-align", "left"), + ], + }, + { + "selector": "td", + "props": [ + ("padding", "10px"), + ("background", "white"), + ("border-top", "1px solid black"), + ], + }, + { + "selector": ".col0, .col1, .col2, .col3, .col4", + "props": [ + ("width", "20%"), + ], + }, + ] + ) + .set_properties( + **{"text-align": "left"}, + ) + ) + + return styled_df + + + +@pn.cache +def to_styled_projects_2_table(data: pd.DataFrame): + + data["Company"] = data.apply(table_func.generate_company_html, axis=1) + data["Completion"] = data.apply(table_func.generate_progress_bar, axis=1) + data = data.drop(columns=["CompanyImage"]) + styled_df = data.style + styled_df = ( + styled_df.hide(axis="index") + .set_table_styles( + [ + { + "selector": "th", + "props": [ + ("background-color", "white"), + ("font-weight", "bold"), + ("border-bottom", "1px solid black"), + ("text-align", "left"), + ], + }, + { + "selector": "td", + "props": [ + ("padding", "10px"), + ("background", "white"), + ("border-top", "1px solid black"), + ], + }, + { + "selector": ".col0, .col1, .col2, .col3", + "props": [ + ("width", "20%"), + ], + }, + ] + ) + .set_properties( + **{"text-align": "left"}, + ) + ) + + return styled_df + + +authors_data = get_authors_data() +styled_authors_table = to_styled_authors_table(authors_data) +authors_table = pn.panel(styled_authors_table, sizing_mode="stretch_width") +authors_card = create_card_with_jumbo_header("### Authors Table", authors_table) + +projects_data_2 = get_project_data_2() +styled_projects_2_table = to_styled_projects_2_table(projects_data_2) +projects_2_table = pn.panel(styled_projects_2_table, sizing_mode="stretch_width") +projects_2_card = create_card_with_jumbo_header("### Project Table", projects_2_table) + +main = pn.Column( + pn.Spacer(height=25), + authors_card, + projects_2_card, + pn.Spacer(sizing_mode="stretch_both"), + sizing_mode="stretch_both", +) +create_page(name="Tables", main=[main]).servable(title="Tables") diff --git a/examples/reference/layouts/Drawer.ipynb b/examples/reference/layouts/Drawer.ipynb index 70b1d9ba..e379eebd 100644 --- a/examples/reference/layouts/Drawer.ipynb +++ b/examples/reference/layouts/Drawer.ipynb @@ -18,9 +18,12 @@ "id": "a6c6a705-0ea1-4a19-a264-eacac2b2544e", "metadata": {}, "source": [ - "The `Drawer` component (akin to a sidebar) provides ergonomic access to destinations in a site or app functionality.\n", + "The `Drawer` component (akin to a sidebar) provides ergonomic access to destinations in a site or app functionality such as switching accounts.\n", + "\n", + "The `Drawer` is built on top of the [Material UI `Drawer`](https://mui.com/material-ui/react-drawer/) and mimics its API.\n", "\n", "## Parameters:\n", + "\n", "For details on other options for customizing the component see the layout and styling how-to guides.\n", "\n", "### Core\n", @@ -165,7 +168,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.2" + "version": "3.12.9" } }, "nbformat": 4, diff --git a/examples/reference/widgets/Button.ipynb b/examples/reference/widgets/Button.ipynb index b6c3a39b..c3753105 100644 --- a/examples/reference/widgets/Button.ipynb +++ b/examples/reference/widgets/Button.ipynb @@ -52,6 +52,7 @@ "* **`icon_size`** (str): Size of the icon as a string, e.g. 12px or 1em.\n", "* **`label`** (str): The title of the widget.\n", "* **`variant`** (str): The button style, either 'solid', 'outlined', 'text'.\n", + "* **`href`** (str): A url. Turns the button into a link.\n", "\n", "##### Styling\n", "\n", diff --git a/src/panel_material_ui/layout/base.py b/src/panel_material_ui/layout/base.py index 9c6ebf75..3cc97729 100644 --- a/src/panel_material_ui/layout/base.py +++ b/src/panel_material_ui/layout/base.py @@ -562,7 +562,6 @@ class Dialog(MaterialListLike): _esm_base = "Dialog.jsx" - class Drawer(MaterialListLike): """ The `Drawer` component can be used to display important content in a modal-like overlay that requires @@ -623,7 +622,6 @@ def create_toggle( toggle.jslink(self, value='open', bidirectional=True) return toggle - __all__ = [ "Accordion", "Alert", diff --git a/src/panel_material_ui/widgets/ChangeIndicator.jsx b/src/panel_material_ui/widgets/ChangeIndicator.jsx new file mode 100644 index 00000000..cece6950 --- /dev/null +++ b/src/panel_material_ui/widgets/ChangeIndicator.jsx @@ -0,0 +1,74 @@ +import {Card, CardContent, Typography, Box, Icon, Divider} from "@mui/material"; + +/** + * A reusable change indicator card displaying a metric, its change, and an icon. + * + * Props: + * - title: Label for the metric + * - icon: Name of the Material Icon to display + * - value: Current value of the metric + * - changePercent: Percentage change (positive or negative) + * - since: Text describing the period, e.g. "Since last week" + */ +const ChangeIndicator = ({title, icon, value, changePercent, since}) => { + const isPositive = changePercent > 0; + const changeColor = isPositive ? "success.main" : "error.main"; + const changeSymbol = isPositive ? "+" : ""; + + return ( + + + {icon} + + + + + {title} + + + + {value} + + + + + {`${changeSymbol}${changePercent}%`} + +  {since} + + + + ); +}; + +export function render({model}) { + const [title]=model.useState("title") + const [icon]=model.useState("icon") + const [value]=model.useState("value") + const [change_percent]=model.useState("change_percent") + const [since]=model.useState("since") + return +} diff --git a/src/panel_material_ui/widgets/Icon.jsx b/src/panel_material_ui/widgets/Icon.jsx new file mode 100644 index 00000000..c861ae0a --- /dev/null +++ b/src/panel_material_ui/widgets/Icon.jsx @@ -0,0 +1,16 @@ +import Icon from "@mui/material/Icon"; + +export function render({model}) { + const [icon] = model.useState("value") + const [color] = model.useState("color") + const [fontSize] = model.useState("font_size") + const [sx] = model.useState("sx") + return ( + {icon} + + ) +} diff --git a/src/panel_material_ui/widgets/List.jsx b/src/panel_material_ui/widgets/List.jsx index 2c590a71..de7640ef 100644 --- a/src/panel_material_ui/widgets/List.jsx +++ b/src/panel_material_ui/widgets/List.jsx @@ -102,6 +102,7 @@ export function render({model}) { model.send_msg({type: "click", item: path}) }} selected={highlight && isActive} + href={href} sx={{ p: `0 4px 0 ${(indent+1) * level_indent}px`, "&.MuiListItemButton-root.Mui-selected": { diff --git a/src/panel_material_ui/widgets/indicators.py b/src/panel_material_ui/widgets/indicators.py index 26adf31c..77a3141b 100644 --- a/src/panel_material_ui/widgets/indicators.py +++ b/src/panel_material_ui/widgets/indicators.py @@ -85,8 +85,43 @@ def _update_value(self, *_, **__): if self.value == -1 and self.variant == "determinate": self.variant = "indeterminate" +# Generalize or move to dashboard example +# Could not get icon from ReactComponent so included it here + +class ChangeIndicator(MaterialWidget): + value = param.String(default="") + title = param.String() + icon = param.String() + change_percent = param.Number() + since = param.String() + + _esm_base = "ChangeIndicator.jsx" + + _importmap = { + "imports": { + "@mui/material": "https://esm.sh/@mui/material@6.4.9", + } + } + +class Icon(MaterialWidget): + """The Icon component will display an icon from the Material UI Icon set. + + References: + + - https://mui.com/material-ui/icons/#icon-font-icons + - https://mui.com/material-ui/api/icon/ + """ + value = param.String(default="", doc="The snake cased name of the icon font ligature.") + color = param.String(default="inherit", doc="""The color of the component. + It supports both default and custom theme colors""") + font_size= param.String(default="medium", doc="""The fontSize applied to the icon. + Defaults to medium, but can be configured to inherit font size.""") + + _esm_base="Icon.jsx" __all__ = [ + "ChangeIndicator", + "Icon", "LoadingSpinner", - "Progress" + "Progress", ]